Skip to content

Configure the JSON:API Jackson runtime

Use jsonApi(mapper) for the default runtime: default identifier conversion and built-in relationship identifier targets, with include requests denied, no link decorators, and no default jsonapi object. Use the builder to change those choices through its five optional settings.

Choose the configuration scope

Application-lifetime runtime One operation or request
Configured native Jackson JsonMapper Include paths and sparse fieldsets
Identifier conversion and custom linkage mappers ResourceWriteOptions selection and document envelope
Representation policy and traversal limits Expected update endpoint identity
Resource link decorators and optional jsonapi.version default Explicit document read/validation context

Configure Jackson and shared collaborators before constructing the runtime. Reuse the resulting immutable, thread-safe JsonApi across requests. Callbacks are retained and reused across calls; keep them safe for concurrent invocation and keep mutable request state outside them. Use the mutable builder only during setup.

The examples below are independent recipes. Start each with a fresh builder from the bootstrap setup below; they are not steps in an all-options configuration.

Configure Jackson first

Jackson controls property discovery, visibility, JSON property names, creators, mix-ins, serializers/deserializers, and value conversion. JSON:API annotations assign member roles to those properties. Jackson-ignored properties stay ignored, and attributes require role annotations.

For example, if your DTO uses bodyText but your JSON naming convention is body_text, configure Jackson's naming strategy before creating the runtime:

import com.kazforge.jsonapi.api.JsonApi;
import com.kazforge.jsonapi.jackson3.JsonApiJackson3;
import tools.jackson.databind.PropertyNamingStrategies;
import tools.jackson.databind.json.JsonMapper;

JsonMapper mapper = JsonMapper.builder()
    .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
    .build();
var builder = JsonApiJackson3.builder(mapper);

With no optional builder settings, JsonApi api = builder.build() uses the same defaults as jsonApi(mapper). For a property declared @JsonApiAttribute String bodyText, the default Jackson naming produces "attributes":{"bodyText":"A draft"}; this mapper produces "attributes":{"body_text":"A draft"}. Selections and policy allowances must then use body_text. Register application value serializers/deserializers on Jackson before runtime construction too; attribute conversion follows that configured mapper rather than a separate JSON:API conversion rule.

Runtime construction derives the mappers it needs without mutating the caller mapper. Finish configuration first; do not rely on later caller-mapper changes propagating to an existing runtime.

Identifier conversion

The default identifierConverter calls toString() on writes. On reads it returns the wire string unchanged, then the binder uses configured Jackson to coerce it to the declared Java identity type. Keep that default unless your application needs a different identifier wire format.

For example, an application can expose its internal string 7 as ref-7. Supply both directions, not just a write-side lambda:

import com.kazforge.jsonapi.mapping.IdentifierConverter;

IdentifierConverter references = new IdentifierConverter() {
    @Override
    public String convert(Object value) {
        return value == null ? null : "ref-" + value;
    }

    @Override
    public Object parse(String wire) {
        if (wire == null || !wire.startsWith("ref-") || wire.length() == 4) {
            throw new IllegalArgumentException("Expected a ref- identifier");
        }
        return wire.substring(4);
    }
};
JsonApi api = builder.identifierConverter(references).build();

Using the Article from getting started, writing new Article("7", "A draft") now emits "id":"ref-7"; reading it into Article.class returns id 7. The same converter serves id and lid across the runtime's mapped resource and PATCH paths, but those remain separate protocol roles with no fallback between them. Choose a format appropriate for every mapped identity, not an article-only prefix on a runtime also used for people.

Custom relationship linkage targets

The linkageMappers registry is empty by default. Flat reads can use ResourceIdentifier and its built-in optional/collection/array shapes. Register a mapper when an application DTO instead needs its own reference type—for example, an AuthorReference, not a fully loaded Person.

import com.kazforge.jsonapi.annotation.JsonApiAttribute;
import com.kazforge.jsonapi.annotation.JsonApiId;
import com.kazforge.jsonapi.annotation.JsonApiRelationship;
import com.kazforge.jsonapi.annotation.JsonApiResource;

public record AuthorReference(String type, String id) {}

@JsonApiResource(type = "articles")
public record ArticleReferenceView(
    @JsonApiId String id,
    @JsonApiAttribute String title,
    @JsonApiRelationship AuthorReference author
) {}

Register a RelationshipLinkageMapper for AuthorReference:

import com.kazforge.jsonapi.core.model.RelationshipData;
import com.kazforge.jsonapi.jackson3.mapping.RelationshipLinkageMapper;
import java.util.Map;

RelationshipLinkageMapper authorReferences = (linkage, targetType) -> {
    if (!(linkage instanceof RelationshipData.SingleLinkage single)) {
        throw new IllegalArgumentException("Expected one author reference");
    }
    var identifier = single.identifier();
    return new AuthorReference(identifier.type(), identifier.id());
};
JsonApi api = builder
    .linkageMappers(Map.of(AuthorReference.class, authorReferences))
    .build();

Reading the article document into ArticleReferenceView.class now gives new AuthorReference("people", "p1") instead of a ResourceIdentifier. Registration is keyed by the relationship target class, not the owning article class or the wire resource type. This mapper is deliberately for to-one author references; support other linkage shapes explicitly if you use the same target in to-many properties.

This is the only builder setting whose callback uses a Jackson-specific type: targetType is Jackson's tools.jackson.databind.JavaType, even when the lambda does not use it. The mapper converts relationship identifiers into DTO values on reads and when binding PATCH changes. It neither serializes relationships on writes nor reads included or fetches related objects. Your application owns any lookup; use representation selection and policy to include already available author objects on writes.

Representation policy

Set representationPolicy to permit inclusion, restrict requested field names, or change traversal limits. Policy controls what a request may select; pass the actual selection separately on each write.

Setting Default Effect
includePolicy IncludePolicy.denyAll() Rejects requested include traversal; relationship identifiers are still written
fieldPolicy FieldPolicy.allowAll() Permits every selected mapped sparse-fieldset name, not arbitrary unknown names
maxIncludeDepth 10 Maximum number of segments in an include path (author is 1; author.articles is 2)
maxIncludedResources 100 Maximum distinct resources emitted in included, not the primary-resource count

Both limits accept zero but reject negative values. Depth zero rejects any nonempty include path; count zero permits no included resources. Exceeding a limit fails mapping rather than silently truncating the response. Repeated encounters of the same included identity do not consume another slot.

For the article/people model, allow one-hop author inclusion and a small requested field vocabulary:

import com.kazforge.jsonapi.representation.FieldAllowance;
import com.kazforge.jsonapi.representation.FieldPolicy;
import com.kazforge.jsonapi.representation.IncludePolicy;
import com.kazforge.jsonapi.representation.RelationshipAllowance;
import com.kazforge.jsonapi.representation.RepresentationPolicy;
import java.util.Set;

RepresentationPolicy policy = RepresentationPolicy.defaults()
    .withIncludePolicy(IncludePolicy.allowing(Set.of(
        RelationshipAllowance.of("articles", "author")
    )))
    .withFieldPolicy(FieldPolicy.allowing(Set.of(
        FieldAllowance.of("articles", "title"),
        FieldAllowance.of("articles", "author"),
        FieldAllowance.of("people", "name")
    )))
    .withMaxIncludeDepth(1)
    .withMaxIncludedResources(10);
JsonApi selectedApi = builder.representationPolicy(policy).build();

Before this change, include=author fails under the default deny-includes policy. Now that request is permitted, and fields[articles]=title,author is permitted too. Without an include selection, the author is still only linkage, not an included resource. See apply selection with policy to pass the selection to selectedApi and observe the result.

Include allowances pair the owning resource type with the relationship's wire name. A nested path needs permission at every segment: author.articles would also need the people / articles allowance and a depth of at least 2. Field allowances pair resource type with field wire name. An unknown or denied requested name fails rather than being silently removed. Inclusion traverses the objects your application supplies.

Field policy is not global redaction or authorization

Field policy checks only names in a supplied sparse fieldset. With no fieldset, mapped fields are still written, even with FieldPolicy.denyAll(). An explicitly empty fieldset selects no attributes or relationships and performs no per-field checks. Identity and resource meta are separate from field selection. Use appropriate DTOs and application authorization for sensitive data; do not use a field allow-list as an unconditional output filter.

The decorators registry is empty by default. Use it when mapped resources need application-owned links without putting endpoint construction into DTO annotations. For the Article from getting started:

import com.kazforge.jsonapi.core.model.Link;
import com.kazforge.jsonapi.core.model.Links;
import com.kazforge.jsonapi.mapping.ResourceDecoration;
import com.kazforge.jsonapi.mapping.ResourceDecoratorRegistry;
import java.util.Map;

ResourceDecoratorRegistry decorators = ResourceDecoratorRegistry.builder()
    .register(Article.class, article -> ResourceDecoration.ofLinks(
        Links.ofLinks(Map.of(
            "self", new Link.StringLink("/articles/" + article.id())
        ))
    ))
    .build();
JsonApi api = builder.decorators(decorators).build();

A write of article 1 previously had no resource links; it now adds "links":{"self":"/articles/1"} inside data. This example assumes path-safe article ids; your application owns URI construction and encoding.

Lookup is an exact match on the effective raw domain class; subclasses need their own registration. Decoration is additive and limited to resource links and links on already-mapped relationships. It does not create relationships or change linkage, meta, inclusion, or document-level links.

Relationship decoration keys use the logical Java/Jackson property identity, unlike representation selections and allowances. For a property author renamed to article-author on the wire, use author in ResourceDecoration.builder().relationship(...); mapping resolves the wire name.

Optional document version default

By default, the runtime injects no jsonapi object. Set jsonApiVersion if your resource documents should advertise a protocol version when the write does not explicitly supply that object:

JsonApi api = builder.jsonApiVersion("1.1").build();

Writing an article now adds "jsonapi":{"version":"1.1"} at the top level. This default applies to api.resources() writes, including collections and create/update authoring. api.documents() and api.relationships() writes do not inherit this default.

A per-write envelope takes precedence. For example, to emit an explicit empty jsonapi object:

import com.kazforge.jsonapi.api.ResourceWriteOptions;
import com.kazforge.jsonapi.core.model.JsonApiObject;
import com.kazforge.jsonapi.document.DocumentEnvelope;

JsonApiObject explicit = JsonApiObject.empty();
ResourceWriteOptions options = ResourceWriteOptions.defaults()
    .withEnvelope(DocumentEnvelope.ofJsonApi(explicit));

String json = api.resources().writeOne(new Article("1", "A draft"), options);

The result has "jsonapi":{}, not "jsonapi":{"version":"1.1"}. An explicit object replaces the default completely—there is no merge. Leaving the envelope's jsonapi component absent (null) inherits the configured default, even if the envelope supplies other members.

jsonapi.version advertises the document's protocol version. It does not configure the library artifact version, business API versioning, HTTP content negotiation, or extension/profile support.

Supply operation-scoped values at the call

ResourceWriteOptions carries selection and envelope; representation policy belongs to the runtime. Its defaults leave include requests, fieldsets, and envelope members absent. Omitted selections remain distinct from explicitly empty requests.

Keep task details on their owning pages:

Per-operation value Task
Include paths, sparse fieldsets, and ResourceWriteOptions Apply selection with policy
Document links, meta, and explicit jsonapi Top-level state
Expected identity for update writes Author an update document
Expected identity for validated PATCH reads Validate endpoint identity
Explicit document read/validation context Preserve document state

For requirements beyond these JsonApi settings, see choose an application or Jackson-specific API and the relevant Javadoc.