Skip to main content

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 jsonapi version object.
  • Document-level invariants — A validate() helper enforces the spec rule that a document MUST contain at least one of data / errors / meta, and that data and errors MUST NOT coexist.
  • Polymorphic data member — Top-level data can be null, a single JsonApiResource, or a list of them; modelled as Object with typed setter overloads.
  • String-or-link map values — Every links map (on JsonApiDocument, JsonApiResource, JsonApiRelationship, JsonApiError) accepts either a JSON string URL or a JsonApiLink JSON object per the spec, via the JsonApiLinkOrStringSwap ObjectSwap.
  • No external dependencies — Only depends on juneau-marshall.

Bean Classes​

ClassSpec members
JsonApiDocumentdata, errors, meta, jsonapi, links, included
JsonApiResourcetype, id, attributes, relationships, links, meta
JsonApiResourceIdentifiertype, id, meta
JsonApiRelationshipdata (linkage), links, meta
JsonApiErrorid, links, status, code, title, detail, source, meta
JsonApiErrorSourcepointer, parameter, header
JsonApiLinkhref, rel, describedby, title, type, hreflang, meta
JsonApiVersionversion, meta (the jsonapi member)
JsonApiLinkOrStringSwapPer-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:

  1. Break round-tripping of any type value the JVM doesn't know about up front, and
  2. 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);

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"

Resources​