JSON:API relationships, linkage, and documents¶
Use the configured JsonApi api from getting started.
Relationship linkage documents¶
api.relationships() reads and writes identifiers as primary data, rather than fetching related
objects or returning their DTOs. Write a to-one linkage document for an existing author:
import com.kazforge.jsonapi.core.model.ResourceIdentifier;
ResourceIdentifier author = ResourceIdentifier.of("people", "p1");
String authorJson = api.relationships().writeToOne(author);
Result:
Read it back as linkage, without a resource-type registry:
linkedAuthor identifies people / p1.
Null to-one linkage¶
A null identifier represents no to-one linkage:
Result:
Reading this document with readToOne returns Java null.
To-many linkage¶
Write the selected identifiers as a collection:
import java.util.List;
List<ResourceIdentifier> authors = List.of(author);
String authorsJson = api.relationships().writeToMany(authors);
Result:
Read a to-many document with the matching operation:
The list contains the people / p1 identifier. An empty to-many write is a different state from null:
Result:
readToMany returns an empty list for this document. To-one and to-many reads reject the other
cardinality rather than converting it. For top-level links/meta on a linkage document, use
api.documents() or a Jackson-specific document writer.
Preserve document state¶
api.documents().read needs a DocumentReadContext that selects resources or identifiers and the
validation rules to apply. A type/id-only object or empty array cannot distinguish those choices on
its own. For a response body (responseBody) containing explicit-null primary data:
import com.kazforge.jsonapi.core.model.DocumentData;
import com.kazforge.jsonapi.core.model.JsonApiDocument;
import com.kazforge.jsonapi.document.DocumentReadContext;
DocumentReadContext resourceContext = DocumentReadContext.resourceDefaults();
JsonApiDocument document = api.documents().read(responseBody, resourceContext);
DocumentData primaryData = document.data();
primaryData is a DocumentData.NullData, not Java null. Writing the document preserves the
explicit-null member:
The result has the same JSON shape as the input above.
In the core model, a null member reference means the member is absent. Explicit-null data uses
DocumentData.NullData (or RelationshipData.NullLinkage inside a relationship). Empty collections
or wrapper objects remain present. A flat DTO cannot represent every one of these states.
For this relationship-endpoint response body (linkageBody):
Select identifier decoding and relationship-endpoint validation separately:
import com.kazforge.jsonapi.core.aggregate.ValidationContext;
import com.kazforge.jsonapi.core.validation.PrimaryDataContext;
import com.kazforge.jsonapi.document.PrimaryDataKind;
ValidationContext relationshipValidation = ValidationContext.defaults()
.withPrimaryDataContext(PrimaryDataContext.RELATIONSHIP);
DocumentReadContext linkageContext = DocumentReadContext.of(
relationshipValidation,
PrimaryDataKind.RESOURCE_IDENTIFIER
);
JsonApiDocument linkage = api.documents().read(linkageBody, linkageContext);
linkage.data() is a DocumentData.SingleIdentifier for people / p1.
identifierDefaults() selects identifier decoding but keeps the default validation rules; select
relationship-endpoint validation explicitly as above. api.documents().write validates with
DocumentUsage.RESPONSE_OR_OTHER (response or other document use). Use
resource operations to author create/update documents, or a Jackson-specific document
writer when you need a custom validation context.
Links-only and meta-only relationships¶
DTO-mapped relationships always carry data. For a response that intentionally leaves it out,
construct the relationship in the core model:
import com.kazforge.jsonapi.core.aggregate.ValidationContext;
import com.kazforge.jsonapi.core.model.DocumentData;
import com.kazforge.jsonapi.core.model.JsonApiDocument;
import com.kazforge.jsonapi.core.model.Link;
import com.kazforge.jsonapi.core.model.Links;
import com.kazforge.jsonapi.core.model.Meta;
import com.kazforge.jsonapi.core.model.Relationship;
import com.kazforge.jsonapi.core.model.Relationships;
import com.kazforge.jsonapi.core.model.ResourceObject;
import java.util.Map;
Link authorUrl = new Link.StringLink("/articles/1/author");
Links authorLinks = Links.ofLinks(Map.of("related", authorUrl));
Relationship authorLink = Relationship.linkOnly(authorLinks);
Relationships relationships = Relationships.ofRelationships(
Map.of("author", authorLink)
);
ResourceObject articleResource = ResourceObject.of("articles", "1")
.withRelationships(relationships);
DocumentData articleData = new DocumentData.SingleResource(articleResource);
JsonApiDocument authorDocument = JsonApiDocument.withData(articleData);
String linksOnlyJson = api.documents().write(authorDocument);
Result:
{
"data": {
"type": "articles",
"id": "1",
"relationships": {
"author": {
"links": {
"related": "/articles/1/author"
}
}
}
}
}
For relationship metadata without linkage, use a meta-only value instead:
Meta visibility = Meta.of(Map.of("visible", false));
Relationship authorMeta = Relationship.metaOnly(visibility);
That relationship object is:
Core constructors check local invariants; document writers also validate whole-document rules.
With only jsonapi-java-core and no adapter, run aggregate validation explicitly on the same model:
import com.kazforge.jsonapi.core.aggregate.JsonApiDocumentValidator;
JsonApiDocumentValidator validator = new JsonApiDocumentValidator();
ValidationContext validation = ValidationContext.defaults();
validator.validate(authorDocument, validation);
Links-only/meta-only relationships are general document forms, not valid replacements for supplied
primary relationships in create/update requests. Those requests require relationship data.
Additive mapping decorators can add links to an existing relationship; they cannot create a
data-less relationship or remove linkage.
Error documents¶
An error document has errors, not data. Your application chooses the status, code, safe detail,
and HTTP response policy:
import com.kazforge.jsonapi.core.model.ErrorObject;
import com.kazforge.jsonapi.core.model.ErrorSource;
import com.kazforge.jsonapi.core.model.JsonApiDocument;
ErrorSource source = ErrorSource.builder()
.pointer("/data/attributes/title")
.build();
ErrorObject error = ErrorObject.builder()
.status("422")
.code("title-required")
.title("Title is required")
.source(source)
.build();
JsonApiDocument errorDocument = JsonApiDocument.withError(error);
String errorJson = api.documents().write(errorDocument);
Result:
{
"errors": [
{
"status": "422",
"code": "title-required",
"title": "Title is required",
"source": {
"pointer": "/data/attributes/title"
}
}
]
}
Read error documents through api.documents():
import com.kazforge.jsonapi.document.DocumentReadContext;
import java.util.List;
DocumentReadContext responseContext = DocumentReadContext.resourceDefaults();
JsonApiDocument readErrors = api.documents().read(errorJson, responseContext);
List<ErrorObject> reportedErrors = readErrors.errors();
reportedErrors contains the title-required error; the document has no data member.
source.pointer is syntax-checked; the library does not resolve it against the request payload.
It does not convert every library exception into an HTTP error automatically.
Diagnostics¶
See Diagnostics for the four exception families, stable codes, location semantics,
and selective application-owned JSON:API error attribution. A diagnostic path is not automatically
a valid request source.pointer; public detail and HTTP policy stay with the application.
Choose an application or Jackson-specific API¶
Use JsonApi for the operations shown in this guide. The Jackson 3 adapter also exposes
Jackson-specific readers, writers, mappers, and binders for direct control over types, validation,
and mapping policy:
| Need | Use |
|---|---|
| Resource DTOs of one declared type, linkage, raw documents, typed PATCH | JsonApi application API |
| Exact absent/null/empty state, links-only relationships, errors | api.documents() with core values and explicit read context |
Parameterized Jackson JavaType, custom codec validation, per-call mapping policy |
Jackson-specific readers, writers, mappers, binders |
| Primary/included DTOs of different resource types | Jackson-specific domain-document reader with explicit ResourceTypeRegistry |
The Jackson-specific factories are on
JsonApiJackson3.
Typed document readers bind included resources independently, not into DTO relationships. Jackson 3
APIs follow Jackson 3's unchecked exception model; JsonApi stream operations wrap IOException in
UncheckedIOException. Caller-owned sources and sinks remain open.