feat: narrow the object ⇒ relationship inference - #10
Merged
Conversation
Any nested object used to become a relationship, and one without an `id`
threw. That left no way to send a JSON column (`metadata`, `settings`) as
an attribute, and no way to create a related resource that has no `id`
yet.
An object — or a list of objects — is now only a relationship when it
carries an identifier member: `type`, `id`, or `lid`. Anything else is a
plain JSON attribute and goes under `attributes` as it was given. An
object that carries a `type` and forgets its `id` still throws, so a
half-written relationship never clears one by accident.
A resource with a `lid` and no `id` is now sideposted the way JSON:API
1.1 describes: the linkage holds `{ type, lid }` and the full resource
goes to the top-level `included`, keeping resource identifiers pure.
Deduplication tracks `id` and `lid` apart.
`$.attributes` also merged per key over the derived ones, instead of
replacing the whole `attributes` object whenever a flat field was
present, which silently dropped it.
Every body shape that serialized before serializes the same way. Only
shapes that used to throw behave differently.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #9.
What changed
The inference was total: any nested object became a relationship, and one without an
idthrew. There was no way to say "this object is an attribute", and no way to create a related resource that has noidyet.An object — or a list of objects — is now only a relationship when it carries an identifier member:
type,id, orlid. Anything else is a plain JSON attribute:The guard the README promises is intact. An object that carries a
typeand forgets itsidstill throws, because thetypegives the intent away — only an object with no identifier member at all is reclassified.Sideposting through
lidA resource with a
lidand noidis no longer an error. The linkage holds{ type, lid }and the full resource goes to the top-levelincluded, so resource identifiers stay pure. A resource identifier object in 1.1 may carry alidin place of anid, so the document is well formed on its own terms:{ "data": { "type": "articles", "attributes": { "title": "Hello" }, "relationships": { "comments": { "data": [{ "type": "comments", "lid": "c-1" }] } } }, "included": [ { "type": "comments", "lid": "c-1", "attributes": { "body": "First" } } ] }Whether a server acts on that is its own business.
lidis defined by 1.1 but left unused by the base spec, which expects extensions to give it meaning — the point of the open #1740. So the shape is well formed and the semantics are the server's; the client has no reason to refuse it either way. Deduplication now tracksidandlidapart, so two new resources sharing anidofundefinedare no longer collapsed into one.This is deliberately left out of the README until the semantics are settled somewhere other than in each server.
Where this comes from
Sideposting has been asked for since 2014 and never landed in the base spec. The threads worth reading, and what this PR takes from each:
relationships, the shape this PR keeps behind$.lidin the linkage with the body inincluded, which is the shape implemented here.included; interactions with?include(closed) and #1216 — Sideposting: graph requirements and restrictions (open). Why serialising the shape is the easy half.lidis deliberately unspecified in the base spec.Nothing here is settled, which is exactly why Fetchja serialises the shape and stays out of the semantics.
$.attributes$.relationshipsmerged per key, but$.attributesreplaced the wholeattributesobject — so any flat field present would silently drop it. It now merges per key too, envelope winning, matching$.relationships.Compatibility
Every body shape that serialized before serializes byte for byte the same way. Only shapes that used to throw behave differently:
{ author: { type, id } }{ tags: [{ type, id }] }{ author: { id: null } }data: null{ tags: [] }data: []{ author: { type, name } }{ metadata: { locale } }{ steps: [{ order: 1 }] }{ comments: [{ type, lid, body }] }Notes on the issue
The report also asked about full child objects inside
relationships.*.data. That shape is already reachable onmainthrough$.relationships, which passes the linkage through untouched:It stays there rather than moving into the flat shape. Attributes inside a resource identifier contradict the spec, so the escape hatch is the right home for it —
lidcovers the same ground without the contradiction.Option (a) from the issue — treating any object with a
datakey as a relationship — was not taken.datais a legal attribute name, so{ payload: { data: 'x' } }would go from a loud throw to a silently malformed request.Tests
7 new cases: JSON-column attribute, list of plain objects, the throw preserved when
typeis present withoutid, sideposting to-one and to-many throughlid, deduplication bylid, and$.attributesmerging. 103 pass,eslintandtsc --noEmitclean.