juneau-bean-jsonapi
The juneau-bean-jsonapi module provides Java beans for the JSON:API v1.1 wire format, used by APIs that serve application/vnd.api+json. JSON:API standardises a document shape, resource objects, relationships, errors, and link semantics so clients and servers can interoperate without ad-hoc per-API conventions.
Overview
This module models the JSON:API wire format as typed @Marshalled POJOs that round-trip through the default JsonSerializer / JsonParser — no custom marshaller required. The bean shapes mirror the spec one-for-one, including the document-level mutual-exclusion rule between data and errors and the per-spec union types on data and links.
Key Features
- Full spec coverage — Document, resource, resource identifier, relationship, error, error source, link, and the
jsonapiversion object. - Document-level invariants — A
validate()helper enforces the spec rule that a document MUST contain at least one ofdata/errors/meta, and thatdataanderrorsMUST NOT coexist. - Polymorphic
datamember — Top-leveldatacan benull, a singleJsonApiResource, or a list of them; modelled asObjectwith typed setter overloads. - String-or-link map values — Every
linksmap (onJsonApiDocument,JsonApiResource,JsonApiRelationship,JsonApiError) accepts either a JSON string URL or aJsonApiLinkJSON object per the spec, via theJsonApiLinkOrStringSwapObjectSwap. - No external dependencies — Only depends on
juneau-marshall.
Bean Classes
| Class | Spec members |
|---|---|
JsonApiDocument | data, errors, meta, jsonapi, links, included |
JsonApiResource | type, id, attributes, relationships, links, meta |
JsonApiResourceIdentifier | type, id, meta |
JsonApiRelationship | data (linkage), links, meta |
JsonApiError | id, links, status, code, title, detail, source, meta |
JsonApiErrorSource | pointer, parameter, header |
JsonApiLink | href, rel, describedby, title, type, hreflang, meta |
JsonApiVersion | version, meta (the jsonapi member) |
JsonApiLinkOrStringSwap | Per-property swap applied to every links map |
Why JsonApiResource.type is a plain String
JSON:API uses type on a resource object as an open-ended entity-type string ("articles", "people", etc.) chosen by the API — not as a closed Java class hierarchy. The Juneau bean intentionally models type as a plain String and is not annotated with @Marshalled(typePropertyName="type", dictionary=...). Conflating JSON:API resource typing with Juneau bean dispatch would:
- Break round-tripping of any
typevalue the JVM doesn't know about up front, and - Force every API to enumerate its resource types in a Java dictionary at compile time.
If you want a per-API typed resource hierarchy, build one on top of JsonApiResource in your application code rather than baking it into the wire bean.
Basic Usage
Building a single-resource document
import org.apache.juneau.bean.jsonapi.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.marshall.json.*;
JsonApiResource article = new JsonApiResource("articles", "1")
.setAttributes(JsonMap.of("title", "JSON:API paints my bikeshed!"))
.putRelationship("author",
new JsonApiRelationship().setData(new JsonApiResourceIdentifier("people", "9")));
JsonApiDocument doc = new JsonApiDocument()
.setJsonapi(new JsonApiVersion("1.1"))
.setData(article);
String wire = JsonSerializer.DEFAULT.write(doc);
JsonApiDocument back = JsonParser.DEFAULT.read(wire, JsonApiDocument.class);
Compound document with included
JsonApiResource author = new JsonApiResource("people", "9")
.setAttributes(JsonMap.of("firstName", "Dan", "lastName", "Gebhardt"));
JsonApiResource comment1 = new JsonApiResource("comments", "5")
.setAttributes(JsonMap.of("body", "First!"));
JsonApiResource comment2 = new JsonApiResource("comments", "12")
.setAttributes(JsonMap.of("body", "I like XML better"));
JsonApiResource article = new JsonApiResource("articles", "1")
.setAttributes(JsonMap.of("title", "JSON:API paints my bikeshed!"))
.putRelationship("author",
new JsonApiRelationship().setData(new JsonApiResourceIdentifier("people", "9")))
.putRelationship("comments",
new JsonApiRelationship().setData(java.util.List.of(
new JsonApiResourceIdentifier("comments", "5"),
new JsonApiResourceIdentifier("comments", "12"))));
JsonApiDocument doc = new JsonApiDocument()
.setJsonapi(new JsonApiVersion("1.1"))
.setData(article)
.addIncluded(author, comment1, comment2);
String-or-link values inside a links map
Every links map accepts both shapes the spec allows. The JsonApiLinkOrStringSwap walks the map on parse and converts each JSON-object value to a JsonApiLink, leaving string values as String. On serialize, String values come out as JSON strings and JsonApiLink values come out as JSON objects.
JsonApiRelationship rel = new JsonApiRelationship()
.setLinks(new java.util.LinkedHashMap<>(java.util.Map.of(
"self", "/articles/1/relationships/author", // string URL
"related", new JsonApiLink("/articles/1/author").setTitle("Author") // full link object
)));
Error documents
JsonApiDocument doc = new JsonApiDocument().addErrors(
new JsonApiError()
.setStatus("422")
.setTitle("Invalid Attribute")
.setDetail("Title must contain at least three characters.")
.setSource(new JsonApiErrorSource().setPointer("/data/attributes/title"))
);
validate()
JsonApiDocument doc = new JsonApiDocument();
doc.validate(); // throws IllegalStateException: at least one of data/errors/meta required
doc.setData(article).addErrors(new JsonApiError());
doc.validate(); // throws IllegalStateException: data and errors must not coexist
Content-Type
The matching Content-Type constant lives in juneau-rest-common and has been available since 9.2.x:
import org.apache.juneau.http.header.ContentType;
ContentType.APPLICATION_VND_API_JSON; // "application/vnd.api+json"