8

How can I use normalizr to deal with nested standardised JSON API responses that are key via the { data: ... } standard?

For example a Book

{
    data: {
        title: 'Lord of the Rings',
        pages: 9250,
        publisher: {
            data:  {
                name: 'HarperCollins LLC',
                address: 'Big building next to the river',
                city: 'Amsterdam'
            },
        },
        author: {
            data: {
                name: 'J.R.R Tolkien',
                country: 'UK',
                age: 124,
            }
        }
    }
}   

How would I design schemas to deal with the nested data key?

AndrewMcLagan
  • 13,459
  • 23
  • 91
  • 158
  • The json which you post doesn't follow the [`JSONAPI` specifications](http://jsonapi.org/format/#document-top-level). Quickly, you can not nest other resources in the main one, but for including other resources in the payload you've to use the specific key [`include`](http://jsonapi.org/format/#fetching-includes). In the main resource you can send only [a limited set of information](http://jsonapi.org/format/#document-resource-object-relationships) about the relationships between itself and other resources under the key `relationships `. – NickGnd Jul 04 '16 at 15:55
  • Your right, I will update the question – AndrewMcLagan Jul 04 '16 at 21:02

2 Answers2

6

For each entity in your response, you should create it's own schema.

In your example, we have three entities - books, authors and publishers:

// schemas.js
import { Schema } from 'normalizr';

const bookSchema = new Schema('book');
const publisherSchema = new Schema('publisher');
const authorSchema = new Schema('author');

If some entity contains nested data which should be normalized, we need to use define method of it schema.This method accepts an object with nesting rules.

If we need to normalize publisher and author props of book entity, we should pass an object to define function with same structure as our response:

// schemas.js
bookSchema.define({
  data: {
    publisher: publisherSchema,
    author: authorSchema
  }
});

Now we can normalize our response:

import { normalize } from 'normalizr';
import { bookSchema } from './schemas.js';

const response = {
    data: {
        title: 'Lord of the Rings',
        pages: 9250,
        publisher: {
            data:  {
                name: 'HarperCollins LLC',
                address: 'Big building next to the river',
                city: 'Amsterdam'
            },
        },
        author: {
            data: {
                name: 'J.R.R Tolkien',
                country: 'UK',
                age: 124,
            }
        }
    }
}

const data = normalize(response, bookSchema);
1ven
  • 6,776
  • 1
  • 25
  • 39
3

I believe what you're after is the use of the assignEntity function which can be passed in the options of normalize. In this instance it lets us, where appropriate, filter out the redundant data properties and go straight to the values underneath.

Effectively assignEntity let's you control how each key of data is normalized. Take a look here for a little more on how it works.

I put this together as a demonstration, take a look: http://requirebin.com/?gist=b7d89679202a202d72c7eee24f5408b6. Here's a snippet:

book.define({
  data: {
    publisher: publisher,
    author: author,
    characters: normalizr.arrayOf(character)
  }}
);

publisher.define({
  data: {
    country: country
  }
});

const result = normalizr.normalize(response, book, { assignEntity: function (output, key, value, input) {
  if (key === 'data') {
    Object.keys(value).forEach(function(d){
      output[d] = value[d];
    })
  } else {
    output[key] = value;
  }
}});

Also see in particular Ln 29, where the array of characters has some objects with the information nested within data and some without. All are normalized correctly.

I also added some parts to show how it works with arrays and deeply nested data, see the country model within publisher.

With the data provided you will need a slug due to the absence of id's, which each schema also contains in the example.

Normalizr is fantastic, I hope that helps explain a little more about it :)

horyd
  • 1,364
  • 10
  • 12
  • Great answer, awesome example! – AndrewMcLagan Jul 04 '16 at 21:17
  • Thanks! Let me know if you think any part could benefit from further explanation :) – horyd Jul 04 '16 at 21:42
  • I'm going to update the question with a proper JSON-API response – AndrewMcLagan Jul 05 '16 at 10:07
  • Yes, very much struggling with this im getting a subkey of undefined for all the nested characters, publishers etc... the only change i made to your example was i actually have an `{ id }` key in my entities... SEE: http://requirebin.com/?gist=db7d88f9866ff59aad62e01e6b6d630b – AndrewMcLagan Jul 25 '16 at 00:35
  • 1
    Because your ```id``` property is nested within the ```data``` object you need to tell normalizr to specifically check in there to find it. Check this out: http://requirebin.com/?gist=26b016c32f130c8b23024ef045e9d221 :) – horyd Jul 25 '16 at 04:40
  • Thank you :-) although my use case is slightly different... arrayOf items are always keyed with `{ data }` for example `{ characters: data: [...] }` your help is very very appreciated. Cant seem to crack this one – AndrewMcLagan Jul 25 '16 at 06:03
  • Then within your define, write: `book.define({ data: { publisher: publisher, author: author, characters: { data: normalizr.arrayOf(character) }}} );` – horyd Jul 25 '16 at 06:22
  • haha so obvious... Only now the value inside `entities.books.characters` is an object and not an array? is that normal and okay? all the docs and your examples list it as an array.... – AndrewMcLagan Jul 25 '16 at 06:46
  • I updated the Requirebin example again. This is occuring because of the assignEntity function processes values with the data key as objects. You can skip over it by checking if it's an Array (as shown), that's probably the easiest way :) – horyd Jul 25 '16 at 21:19
  • This is getting out of hand :-) that does not work as expected. Its not possible (maybe) to assign an array to `output` normalizr seems to ignore this. I have engineered the bin example: http://requirebin.com/?gist=76f79fb6cd4990a4c88131fb4b151958 I will update my answer when we can crack this... – AndrewMcLagan Jul 26 '16 at 00:10
  • Yea normalizr is working correct in that regard, since it doesn't know that you don't have any other keys aside from ```data``` in the ```characters``` object, so it won't let the whole thing be overwritten with as an array. It could be worth instead running the object through a sort of data-property flattening function first, one that squashes the data key out when it sees it's the only key of the nested object (and switches the object to an array if that's what the data key is). And then normalizing that (which would be much simpler) – horyd Jul 27 '16 at 08:37
  • That was also my thoughts... Running an arbitrary pre/post normalisation function. Although in the end as I control my API it's now fully normalised server side. Better. Less expensive on the client, better the CPU you know the the one you don't. That being said I will come up with an answer. Thank you so much for your efforts, very appreciated !! – AndrewMcLagan Jul 27 '16 at 13:35