Series MapLesson 48 / 112
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Build CoreOrdered learning track

JSON Processing

JSON Processing Jackson and JSON-B

JSON serialization/deserialization dalam JAX-RS, termasuk Jackson, JSON-B, ObjectMapper configuration, compatibility, date/time, enum, null handling, dan security risk

6 min read1162 words
PrevNext
Lesson 48112 lesson track22–61 Build Core
#json#jackson#json-b#jax-rs+5 more

Part 048 — JSON Processing: Jackson and JSON-B

Fokus part ini: memahami bagaimana JSON request body menjadi object Java dan bagaimana object Java menjadi JSON response di JAX-RS/Jakarta REST. Kita akan membahas Jackson, JSON-B, entity provider, ObjectMapper, date/time, enum, null handling, unknown field, immutable DTO, polymorphism, security risk, compatibility, failure mode, debugging, dan PR review.

JSON terlihat sederhana.

Tapi di production, JSON adalah kontrak.

Kesalahan kecil dalam serialization/deserialization bisa menjadi:

- breaking API change
- silent data loss
- wrong enum handling
- timezone bug
- monetary precision bug
- backward compatibility bug
- security vulnerability
- incident karena client lama tidak bisa parse response baru

Senior engineer harus melihat JSON bukan hanya sebagai format data, tapi sebagai compatibility boundary.


1. Core Mental Model

Di JAX-RS, JSON processing terjadi melalui entity provider.

Simplified flow:

sequenceDiagram participant C as Client participant R as JAX-RS Runtime participant P as MessageBodyReader/Writer participant J as JSON Library participant M as Resource Method C->>R: HTTP request with JSON body R->>R: match resource method R->>P: find MessageBodyReader for media type + Java type P->>J: deserialize JSON to DTO J-->>P: DTO or error P-->>R: Java object R->>M: invoke method M-->>R: Java response object R->>P: find MessageBodyWriter P->>J: serialize DTO to JSON J-->>P: JSON bytes P-->>C: HTTP response

JAX-RS does not magically understand JSON.

It delegates JSON processing to a provider.

Common providers:

- Jackson-based provider
- JSON-B provider
- MOXy provider in some Jakarta/Jersey environments
- custom MessageBodyReader/Writer

Internal runtime determines which provider wins.


2. Standard vs Implementation

Separate the layers:

JAX-RS / Jakarta REST
  Defines entity provider concept.
  Uses MessageBodyReader and MessageBodyWriter.

JSON-B
  Jakarta standard binding API for JSON <-> Java objects.
  Package: jakarta.json.bind.*

Jackson
  Popular non-Jakarta JSON library.
  Package: com.fasterxml.jackson.*
  Very common in enterprise Java services.

Jersey JSON integration
  Jersey modules can integrate Jackson, JSON-B, MOXy, etc.

Do not assume a service uses Jackson just because DTOs serialize to JSON.

Look for dependencies:

com.fasterxml.jackson.core:jackson-databind
com.fasterxml.jackson.datatype:jackson-datatype-jsr310
org.glassfish.jersey.media:jersey-media-json-jackson
org.glassfish.jersey.media:jersey-media-json-binding
jakarta.json.bind:jakarta.json.bind-api
org.eclipse:yasson

Internal verification:

[ ] Which JSON provider is registered?
[ ] Is Jackson active, JSON-B active, MOXy active, or custom provider active?
[ ] Is provider registered explicitly or auto-discovered?
[ ] Is there a central ObjectMapper/Jsonb configuration?
[ ] Are request and response using the same provider?

3. Why JSON Configuration Is Architecture

JSON settings affect public contract.

Examples:

- field naming: customerId vs customer_id
- null inclusion: present null vs omitted field
- unknown field policy: fail vs ignore
- enum representation: name vs custom code
- date format: ISO string vs timestamp
- BigDecimal format: number vs string
- empty collection: [] vs null vs omitted
- polymorphic type metadata: enabled vs disabled

These are not formatting details.

They determine whether clients can safely upgrade independently.


4. Jackson Basics

Jackson usually centers around ObjectMapper.

Example:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

In Jersey, Jackson provider may be configured through a ContextResolver<ObjectMapper>:

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;

@Provider
public final class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
    private final ObjectMapper mapper;

    public ObjectMapperProvider() {
        this.mapper = new ObjectMapper()
                .registerModule(new JavaTimeModule())
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mapper;
    }
}

Important:

One codebase can have more than one ObjectMapper.

Common hidden bug:

- API ObjectMapper differs from Kafka ObjectMapper
- test ObjectMapper differs from runtime ObjectMapper
- internal client ObjectMapper differs from server ObjectMapper
- custom mapper is created ad hoc with new ObjectMapper()

Senior rule:

ObjectMapper configuration is a platform concern.
Ad hoc mapper creation should be rare and justified.

5. JSON-B Basics

JSON-B is Jakarta's JSON binding standard.

Example:

JsonbConfig config = new JsonbConfig()
        .withNullValues(false)
        .withDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.ROOT);

Jsonb jsonb = JsonbBuilder.create(config);

JSON-B annotations include:

@JsonbProperty("customer_id")
@JsonbTransient
@JsonbDateFormat("yyyy-MM-dd")

Jackson annotations include:

@JsonProperty("customer_id")
@JsonIgnore
@JsonFormat(pattern = "yyyy-MM-dd")

Do not mix annotation families casually.

If JSON-B provider is active, Jackson annotation may be ignored.

If Jackson provider is active, JSON-B annotation may be ignored.

Internal verification:

[ ] Are DTOs using Jackson annotations, JSON-B annotations, or both?
[ ] Does runtime provider honor those annotations?
[ ] Are there tests proving the actual JSON shape?

6. DTO Shape and Java Records

Java records are good DTO candidates:

public record QuoteResponse(
    String quoteId,
    String customerId,
    String status,
    List<QuoteLineResponse> lines
) {}

Benefits:

- immutable by default
- concise
- constructor defines required shape
- easier reasoning

But verify provider support.

Potential issues:

- older Jackson version may need specific support
- older JSON-B implementation may behave differently
- no default constructor may break some provider/config
- reflection/module constraints can matter in advanced runtimes

For mutable DTOs:

public final class QuoteResponse {
    private String quoteId;
    private String customerId;

    public String getQuoteId() { return quoteId; }
    public void setQuoteId(String quoteId) { this.quoteId = quoteId; }
}

This is broadly compatible but easier to mutate accidentally.

Senior review asks:

Is this DTO an API contract or an internal mutable data bag?

7. Unknown Field Policy

When a client sends unknown fields:

{
  "customerId": "C-123",
  "currency": "USD",
  "unexpectedField": "value"
}

Options:

Fail request:
  strict contract, catches typos, less forward-compatible

Ignore unknown:
  more tolerant, better forward compatibility, can hide client mistakes

Jackson setting:

mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

DTO-level override:

@JsonIgnoreProperties(ignoreUnknown = true)
public record CreateQuoteRequest(...) {}

Enterprise guideline:

Use a deliberate policy.
Do not let default library behavior become API governance by accident.

Possible policy:

- External public API: often tolerant to unknown fields for forward compatibility.
- Internal strict API: may fail unknown fields to catch contract drift.
- Security-sensitive API: be careful with ignored fields that imply behavior.

8. Null Handling and Field Absence

JSON has a critical difference:

{ "discount": null }

vs

{}

They can mean different things.

For create request:

missing field -> use default
null field -> explicit null, maybe invalid

For patch request:

missing field -> do not change
null field -> clear value

If API does not define this, bugs appear.

Jackson serialization inclusion:

mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

But be careful:

Omitting null fields in response can be a breaking change if clients expect field presence.

Patch DTO often needs explicit presence tracking.

Example:

public record PatchQuoteRequest(
    OptionalField<String> description,
    OptionalField<BigDecimal> discount
) {}

Where OptionalField distinguishes:

- absent
- present null
- present value

Do not assume Optional<T> solves JSON merge semantics cleanly without verifying provider behavior.


9. Enum Serialization

Default enum serialization often uses enum name:

public enum QuoteStatus {
    DRAFT,
    SUBMITTED,
    APPROVED,
    REJECTED
}

JSON:

{ "status": "SUBMITTED" }

Risk:

Renaming Java enum constant becomes breaking API change.

For stable API, consider explicit code:

public enum QuoteStatus {
    DRAFT("draft"),
    SUBMITTED("submitted"),
    APPROVED("approved"),
    REJECTED("rejected");

    private final String code;

    QuoteStatus(String code) {
        this.code = code;
    }

    @JsonValue
    public String code() {
        return code;
    }
}

Deserialization can use creator:

@JsonCreator
public static QuoteStatus fromCode(String code) {
    return Arrays.stream(values())
            .filter(status -> status.code.equals(code))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Unknown quote status: " + code));
}

But decide error mapping carefully.

Unknown enum should usually become 400 invalid request, not 500 internal server error.

Forward compatibility concern:

When server returns a new enum value, older clients may fail.

For responses consumed by external clients, enum evolution needs policy.


10. Date and Time Serialization

Date/time is a frequent source of production bugs.

Common Java types:

LocalDate
LocalDateTime
OffsetDateTime
Instant
ZonedDateTime

General guidance:

Use LocalDate for date-only business concepts.
Use Instant or OffsetDateTime for exact moments.
Avoid LocalDateTime for cross-system timestamps unless timezone is defined elsewhere.

Jackson Java Time support usually needs:

mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Example JSON:

{
  "effectiveDate": "2026-07-10",
  "submittedAt": "2026-07-10T03:15:30Z"
}

Do not serialize date/time as numeric timestamp accidentally unless the contract says so.

This part only covers JSON serialization.

Temporal business correctness is covered in the dedicated date/time/currency/precision part.


11. BigDecimal and Numeric Precision

JSON number has no explicit decimal type.

Example:

{ "price": 123.45 }

Java should often use BigDecimal for money-like values:

public record PriceResponse(
    String currency,
    BigDecimal amount
) {}

Risks:

- client parses number as floating point
- server serializes with unexpected scale
- trailing zeros lost
- rounding not defined

Some APIs serialize money as string for precision:

{ "amount": "123.4500" }

But that is a contract decision.

Do not switch number/string without compatibility review.


12. Field Naming Strategy

Java usually uses camelCase:

customerId
quoteLineId
effectiveDate

JSON may use camelCase or snake_case:

{
  "customerId": "C-123"
}

or:

{
  "customer_id": "C-123"
}

Jackson naming strategy:

mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

Annotation override:

@JsonProperty("customer_id")
String customerId

Senior guideline:

Prefer one API-wide naming convention.
Avoid per-field exceptions unless required for compatibility.

Internal verification:

[ ] Is naming strategy global or annotation-based?
[ ] Do old APIs follow a different convention?
[ ] Are generated clients aligned with JSON field names?

13. Polymorphism and Security Risk

Polymorphic JSON is dangerous if configured broadly.

Bad idea:

mapper.enableDefaultTyping(...)

Risk:

Deserialization may instantiate unexpected classes.
Historically, unsafe polymorphic deserialization has caused serious vulnerabilities.

If polymorphism is needed, use explicit type models and limited subtypes.

Example:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = FixedDiscount.class, name = "fixed"),
    @JsonSubTypes.Type(value = PercentageDiscount.class, name = "percentage")
})
public sealed interface Discount permits FixedDiscount, PercentageDiscount {}

Even then, ask:

Is polymorphism part of public API contract?
Can clients generate this safely?
Can OpenAPI represent it clearly?
How will new subtype be introduced compatibly?

14. JSON Views, Partial Response, and Contract Drift

Jackson has @JsonView.

It can produce different response shapes from same DTO.

Risk:

- contract becomes implicit
- tests miss fields in specific view
- generated OpenAPI may not reflect runtime shape
- response compatibility becomes hard to reason about

For enterprise APIs, prefer explicit response DTOs:

QuoteSummaryResponse
QuoteDetailResponse
QuoteLineResponse

Rather than one DTO with many conditional serialization rules.

Partial response should be an explicit API governance decision, not accidental @JsonView sprawl.


15. Request DTO vs Response DTO

Do not reuse request and response DTOs casually.

Bad:

public record QuoteDto(
    String quoteId,
    String customerId,
    String status,
    BigDecimal total,
    List<QuoteLineDto> lines
) {}

Used for:

- create request
- update request
- detail response
- event payload
- database projection

This creates contract coupling.

Better:

CreateQuoteRequest
UpdateQuoteRequest
QuoteResponse
QuoteSummaryResponse
QuoteCreatedEvent
QuoteProjection

Reason:

Each boundary evolves differently.

16. JSON Error Handling in JAX-RS

Common JSON failures:

- malformed JSON
- wrong content type
- unknown field if strict
- wrong type: string where object expected
- invalid enum value
- invalid date format
- missing required creator property
- custom deserializer failure

Map them consistently.

Typical mapping:

FailurePossible HTTP StatusNote
Missing/unsupported Content-Type415Depends endpoint and provider
Client cannot accept response media type406Content negotiation failure
Malformed JSON400Request body unreadable
JSON type mismatch400Deserialization failure
Bean validation failure after JSON parse400Constraint violations
Serialization failure for response500Server produced unserializable object

Do not expose raw Jackson exception in API response.

But do log enough for debugging with correlation ID.


17. Failure Modes

FailureSymptomLikely Cause
415 Unsupported Media TypeRequest rejected before resource methodWrong Content-Type or missing provider
406 Not AcceptableClient cannot get requested media typeAccept mismatch
400 malformed bodyJSON parse/deserialization failureInvalid JSON/type/enum/date
500 serialization errorResource returned object provider cannot serializeCyclic graph, bad getter, unsupported type
Field missing in responseNull inclusion or annotationNON_NULL, @JsonIgnore, view
Unknown field accepted silentlyTolerant configFAIL_ON_UNKNOWN_PROPERTIES=false
Unknown field rejected unexpectedlyStrict configProvider config mismatch
Date serialized as array/timestampMissing JavaTime configWRITE_DATES_AS_TIMESTAMPS
Enum breaks clientsNew enum value or renamed constantNo compatibility policy
Tests pass but runtime differsDifferent mapper/providerTest uses ad hoc mapper

18. Debugging Checklist

When JSON behavior is unexpected:

[ ] What is the request Content-Type?
[ ] What is the request Accept header?
[ ] Which MessageBodyReader/Writer is selected?
[ ] Which JSON provider is active: Jackson, JSON-B, MOXy, custom?
[ ] Is provider registered explicitly or auto-discovered?
[ ] Is ObjectMapper/Jsonb config centralized?
[ ] Are annotations from Jackson and JSON-B mixed?
[ ] Are tests using the same mapper as runtime?
[ ] Is date/time module registered?
[ ] Is unknown field policy intentional?
[ ] Is null inclusion policy intentional?
[ ] Are enum values stable API codes?
[ ] Is polymorphic deserialization enabled anywhere?
[ ] Are response DTOs accidentally exposing internal fields?

Runtime-level evidence:

- startup logs
- ResourceConfig registration
- provider class names
- dependency tree
- failing stack trace
- wire-level request/response capture
- endpoint integration test

19. Testing JSON Contracts

Unit test object mapper only when mapper config itself is the subject.

Example:

@Test
void serializesEffectiveDateAsIsoDate() throws Exception {
    var response = new QuoteResponse("Q-1", LocalDate.of(2026, 7, 10));

    String json = objectMapper.writeValueAsString(response);

    assertThat(json).contains("\"effectiveDate\":\"2026-07-10\"");
}

But endpoint contract tests are better:

[ ] Send real HTTP request.
[ ] Use real Content-Type and Accept header.
[ ] Exercise real MessageBodyReader/Writer.
[ ] Assert response body shape.
[ ] Assert error body for invalid JSON.
[ ] Assert unknown/null/enum/date behavior.

Golden JSON tests can help, but avoid brittle formatting assertions.

Assert semantic shape, not whitespace.


20. Backward-Compatible JSON Evolution

Usually safe changes:

- add optional response field
- add optional request field with safe default
- add enum value only if clients tolerate unknown values
- loosen validation carefully

Usually breaking changes:

- remove field
- rename field
- change field type
- change number to string or string to number
- change null/absence semantics
- make optional field required
- change date format
- change enum value spelling
- change object to array or array to object

Compatibility is consumer-dependent.

An internal API with generated clients can break differently from a public API with manually written clients.


21. JSON and OpenAPI Alignment

OpenAPI must match actual JSON behavior.

Common drift:

- OpenAPI says field required, runtime allows missing
- OpenAPI says string date, runtime emits timestamp
- OpenAPI says enum list, runtime has extra enum
- OpenAPI says nullable false, runtime returns null
- OpenAPI says object, runtime returns array in edge case

PR review should require:

[ ] DTO change updates OpenAPI.
[ ] OpenAPI examples match runtime serialization.
[ ] Generated clients still compile.
[ ] Contract tests cover changed fields.
[ ] Compatibility matrix is updated when needed.

22. Internal Verification Checklist

For CSG/internal codebase verification:

[ ] Which JSON provider is active for JAX-RS endpoints?
[ ] Is Jackson used? If yes, which version and modules?
[ ] Is JSON-B used? If yes, which implementation?
[ ] Is MOXy used anywhere by Jersey default/legacy config?
[ ] Where is ObjectMapper/Jsonb configured?
[ ] Is there more than one ObjectMapper?
[ ] Do tests use the production mapper/provider?
[ ] Are DTOs using Jackson annotations, JSON-B annotations, or both?
[ ] What is the unknown field policy?
[ ] What is the null inclusion policy?
[ ] What is the field naming strategy?
[ ] How are dates/times serialized?
[ ] How are BigDecimal/money-like values serialized?
[ ] How are enums represented?
[ ] Is polymorphic deserialization enabled anywhere?
[ ] Are API DTOs reused as event/database/internal DTOs?
[ ] Is OpenAPI generated from code, manually written, or contract-first?
[ ] Are JSON contract changes checked in CI?

23. PR Review Checklist

For JSON/API DTO changes:

[ ] Is this DTO specific to one boundary?
[ ] Is request DTO separated from response DTO?
[ ] Is field naming consistent with API convention?
[ ] Is new field optional or required?
[ ] If required, is rollout/backward compatibility handled?
[ ] Is null vs absent behavior defined?
[ ] Is unknown field policy respected?
[ ] Are enum values stable contract codes?
[ ] Is date/time format explicit?
[ ] Is BigDecimal precision/scale behavior acceptable?
[ ] Are Jackson/JSON-B annotations compatible with active provider?
[ ] Does OpenAPI match runtime JSON?
[ ] Are contract tests updated?
[ ] Does serialization avoid exposing internal/sensitive fields?
[ ] Does deserialization avoid unsafe polymorphism?

For provider/config changes:

[ ] Is the change global or endpoint-specific?
[ ] Which existing APIs are affected?
[ ] Are response examples changed?
[ ] Are generated clients affected?
[ ] Are event serializers affected if mapper is shared?
[ ] Are migration/release notes needed?

24. Senior-Level Rule of Thumb

Treat JSON as a long-lived contract.

Every DTO field is a promise.
Every serialization setting is API governance.
Every enum value is compatibility debt.
Every date/time format is an integration decision.
Every ObjectMapper is a potential platform fork.

Prefer:

- explicit DTOs
- centralized mapper/provider config
- endpoint contract tests
- stable field names
- stable enum codes
- explicit null/absence semantics
- OpenAPI alignment

Avoid:

- ad hoc ObjectMapper creation
- DTO reuse across boundaries
- implicit date/time defaults
- accidental null omission
- unsafe polymorphic deserialization
- generated API docs that do not match runtime

25. What You Should Be Able to Do After This Part

After this part, you should be able to:

[ ] Explain how JAX-RS selects JSON reader/writer providers.
[ ] Distinguish Jackson, JSON-B, Jersey provider modules, and standard JAX-RS extension points.
[ ] Find the active JSON provider in a codebase.
[ ] Review ObjectMapper/Jsonb configuration from an API compatibility perspective.
[ ] Debug 400/406/415/500 JSON-related failures.
[ ] Identify null, enum, date/time, BigDecimal, and unknown-field risks.
[ ] Review JSON DTO changes with senior-level compatibility discipline.

Summary

JSON processing is not a minor implementation detail.

It is one of the most important compatibility surfaces in a JAX-RS service.

JAX-RS provides the entity provider mechanism.

Jackson and JSON-B provide JSON binding behavior.

Your production system provides the real contract through configuration, annotations, tests, OpenAPI, and client expectations.

Senior engineers do not let JSON defaults become accidental architecture.

Lesson Recap

You just completed lesson 48 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.