danivek / json-api-serializer

Node.js/browser framework agnostic JSON API (http://jsonapi.org/) serializer.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Deserializing relationships to an object

stefanvanherwijnen opened this issue · comments

The default behaviour of deserialize() seems to be to return an array of relationships keys. If the top-level key "included" is set, it returns the relationships as an object with an 'id' key however.

For example:

{
	"data": {
		"id": 1,
		"attributes": {
			"name": "randomname"
		},
		"relationships": {
			"roles": {
				"data": {
					"type": "role",
					"id": 1
				}
			}
		}
	}
}

deserializes to { id: 1, name: 'randomname', roles: 1 }.

{
	"data": {
		"id": 1,
		"attributes": {
			"name": "randomname"
		},
		"relationships": {
			"roles": {
				"data": {
					"type": "role",
					"id": 1
				}
			}
		}
	},
	"included": [{
		"type": "role",
		"id": 1
	}]
}

deserializes to: { id: 1, name: 'randomname', roles: { id: 1 } }

Now, I would like to get the second result, but the JSON API spec doesn't seem to require the "included" key on updates: https://jsonapi.org/format/#crud-updating-resource-relationships

So I am wondering, is there a specific reason why the result of deserialization differs when the "included" key exists in the request?

The behaviour of serialization is that if the relationship's attributes are populated (not just an id but an object), the data is supposed to be included.

To keep this behaviour on deserialize, if the included key is set, it considers that the relationship's data was populated, so it will set all the attributes in an object.

Maybe a test is missing to check if the included data only contains "resource identifier object" (only type and id attributes). In such case, the result will be the same as if the included data is not set on input data for the relationship.

Exemple:

{ id: 1, name: 'randomname', roles: 1 }

The reason why I am asking is because I would like to use it with ObjectionJS's graphInsert (https://vincit.github.io/objection.js/#graph-inserts). This requires the relation to be an array of resource identifier objects.

Would it make sense to add a check to see if the relationship is an array of "resource identifier objects" on deserialization? (i.e. if it contains type and id it should deserialize the relationship to an array of objects instead of an array of ID's, independent of the included property)

Each ORM is different and has specifics needs.

What about having an option transform that takes a function to transform each data after deserialization ?

In such case every specifics needs should be code here...

@mattiloh I think you have already worked with Objection.js. Any thougths ?

Thanks

I understand that the ORM implementation should not be the concern of this serializer. However, imo it does not make sense to deserialize a resource identifier object to a simple ID if it is not in included.

This change already solves the 'problem':
https://github.com/stefanvanherwijnen/json-api-serializer/commit/228f152d3b6a8e349c9cbea46f4e865cdb0d95b4

So the question is: what is the correct way to deserialize a relation? The serialize example in the README also shows this problem: the array of strings in photos and the array of objects in comments are both serialized to the same result (resource identifier objects). So I guess there should be an option to specify the deserialization such that one can get both results.

Edit:
Something like this: https://github.com/stefanvanherwijnen/json-api-serializer/commit/6e09d8ff64086685c759871e6f7d04856bf46122

Hi @stefanvanherwijnen!
Yes, we use Objection.js with this deserializer, but I never used the graph-insert so far. We probably should give it a try in the future, because it seems to be a great feature.

Regarding your proposal: I can see that it could make sense for to-many relationships, because they are normally saved as dedicated rows in other tables (at least in relational databases). But belongs-to-one relationships are normally saved as single ids on the resource itself (e.g. a team relation of a user resource might be saved in the foreign key column team_id). Since this deserializer is resolving these values to ids only, it works great in such cases.

If we would just apply your proposed changes, the serialization of all belongs-to-one relationships would break and needed a refactor. A has-one relationship on the other hand could work fine with your proposed changes.

I don't have a definitive opinion on this yet (since I didn't have the time to dive deeper today), but after these first thoughts, I would be careful with hard-coding a behavior that is working fine with one specific ORM.

I think @danivek's idea of a transform option could be a good idea, since it allows to adjust the output to any ORM. Maybe it could also be a deserializer option of a relationship?

E.g.

Serializer.register("user", {
  ...
  relationships: {
    team: {
      type: 'teams',
      deserializer: data => data,
    }
  }
});

@mattiloh
You are right about the toMany relationship. I had not thought of it, but for a belongsTo relation, an array of keys is indeed the right way, in contrast to the toMany relation. So both options should be available in the end.
Did you have a look at:
https://github.com/stefanvanherwijnen/json-api-serializer/commit/6e09d8ff64086685c759871e6f7d04856bf46122
?
My first suggestion would indeed break the correct behaviour of deserializing to an array, but with this one can choose the deserialization behaviour.

Thanks for the feedback.

Yes, I had a look at stefanvanherwijnen@6e09d8f.
But I think a generic deserialize option for relationships would be more flexible and provide a clean API.

Serializer.register("user", {
  relationships: {
    team: {
      type: 'teams',
      deserialize: data => ({ id: data.id })
    }
  }
});

It allows you to do what you need without being opinionated about the default output for objects. I think it's possible that somebody else in the future would like the deserialized object to look different from your proposal.

Do you think that would solve your problem?

At @danivek, what's your opinion on this? You imagined a transform option. Did you imagine it to be like the proposed deserialize?

Thanks, I think this would be a good solution. It should solve my problem and the added flexibility is indeed a better choice.
If @danivek agrees I can create a PR for your proposal if you don't have time yourself.

I agree with @mattiloh for having a more flexible API.

I imagined a transform option that has the same goal as deserialize, except that to be even more generic, it will apply on the whole data and not only on each relationship's data.

Anyway, deserialize option on relationship is still an acceptable solution for me.

This seems to work:
https://github.com/stefanvanherwijnen/json-api-serializer/commit/f4e3e1b966159a7418e2acb9b5bcd7476fec28e0

With regards to a transform function: wouldn't that collide with the JSON API spec? I think this problem only exists for relationship data, so if one could transform other data as well it might not be compliant with the specification (except for e.g. case conversion, but that is already implemented).

@mattiloh @danivek
Any feedback on the deserialize functionality in my fork? Anything missing or should I make a PR?