Series MapLesson 10 / 32
Build CoreOrdered learning track

Learn Java Data Mapper Json Xml Validation Part 010 Objectmapper Production Configuration

12 min read2378 words
PrevNext
Lesson 1032 lesson track0718 Build Core

title: Learn Java Data Mapper, JSON/XML Processing & Validation - Part 010 description: ObjectMapper production configuration: lifecycle, thread safety, immutable readers/writers, module strategy, strict/tolerant profiles, naming, date-time, enum, unknown fields, coercion, security, performance, and governance. series: learn-java-data-mapper-json-xml-validation seriesTitle: Learn Java Data Mapper, JSON/XML Processing & Validation order: 10 partTitle: ObjectMapper Production Configuration tags:

  • java
  • jackson
  • objectmapper
  • json
  • production
  • configuration
  • serialization
  • deserialization
  • contract
  • api-design date: 2026-06-29

Part 010 — ObjectMapper in Production

Target skill: mampu mendesain konfigurasi ObjectMapper yang stabil, reusable, aman, testable, dan sesuai contract boundary.

ObjectMapper adalah salah satu dependency kecil yang bisa memberi dampak besar.

Satu perubahan seperti:

mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

bisa membuat API yang sebelumnya strict menjadi tolerant.

Satu perubahan seperti:

mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

bisa mengubah field presence di semua response.

Satu perubahan seperti:

mapper.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

bisa mengubah timestamp dari ISO string menjadi number/array.

Part ini membahas ObjectMapper sebagai platform configuration, bukan utility object.


1. Kaufman Deconstruction

Subskill yang perlu dikuasai:

SubskillOutput Praktis
Lifecycle disciplineMapper dibuat, dikonfigurasi, lalu dipakai tanpa dimutasi sembarangan
Profile designStrict request mapper, tolerant event mapper, internal mapper, test mapper
Module strategyJava Time, domain module, XML/dataformat module, custom module
Feature policyUnknown fields, nulls, coercion, enum, date/time, inclusion, naming
Reader/writer usagePer-boundary ObjectReader / ObjectWriter immutable
Compatibility testingFixture tests menangkap perubahan output/input
Security hardeningPolymorphism, unknown type, large payload, unsafe default typing
GovernanceConfig review, versioning, shared starter, migration policy

Tujuan latihan:

  1. Buat satu mapper strict.
  2. Buat satu mapper tolerant untuk event consumer.
  3. Buat codec per boundary memakai ObjectReader/ObjectWriter.
  4. Tambahkan fixture test.
  5. Ubah satu feature dan lihat test mana yang gagal.

2. Production Rule: Configure Once, Then Reuse

ObjectMapper instances dirancang untuk reusable dan thread-safe jika semua konfigurasi diselesaikan sebelum operasi read/write. Karena itu, pattern production yang baik adalah:

public final class JsonMapperFactory {
    private JsonMapperFactory() {}

    public static ObjectMapper createStrictApiMapper() {
        return JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .build();
    }
}

Kemudian inject mapper tersebut.

Buruk:

objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper.readValue(json, SomeType.class);

Masalahnya bukan hanya thread-safety. Masalahnya adalah semantic drift: behavior mapper berubah berdasarkan callsite terakhir.


3. ObjectMapper as Infrastructure, Not Utility

Hindari:

public final class JsonUtils {
    public static String toJson(Object value) {
        try {
            return new ObjectMapper().writeValueAsString(value);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

Lebih baik:

public final class JsonCodec {
    private final ObjectMapper mapper;

    public JsonCodec(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    public String write(Object value) {
        try {
            return mapper.writeValueAsString(value);
        } catch (JsonProcessingException e) {
            throw new JsonWriteException("JSON_WRITE_FAILED", e);
        }
    }
}

Lebih baik lagi untuk boundary penting:

public final class PaymentRequestCodec {
    private final ObjectReader reader;
    private final ObjectWriter writer;

    public PaymentRequestCodec(ObjectMapper mapper) {
        this.reader = mapper.readerFor(CreatePaymentRequest.class);
        this.writer = mapper.writerFor(CreatePaymentRequest.class);
    }

    public CreatePaymentRequest read(String json) {
        try {
            return reader.readValue(json);
        } catch (IOException e) {
            throw new InvalidPayloadException("CREATE_PAYMENT_INVALID_JSON", e);
        }
    }

    public String write(CreatePaymentRequest request) {
        try {
            return writer.writeValueAsString(request);
        } catch (JsonProcessingException e) {
            throw new JsonWriteException("CREATE_PAYMENT_WRITE_FAILED", e);
        }
    }
}

4. Mapper Profiles

Satu mapper global untuk semua kebutuhan sering terlalu kasar.

Gunakan profile berdasarkan boundary.

4.1 Strict API Request Mapper

Cocok untuk command/request yang dikirim client internal atau public API yang contract-nya jelas.

Policy:

  • fail on unknown properties
  • reject ambiguous coercion
  • ISO date/time
  • explicit enum handling
  • stable error mapping

Example:

public static ObjectMapper strictApiMapper() {
    return JsonMapper.builder()
        .addModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
        .build();
}

4.2 Tolerant Event Consumer Mapper

Cocok untuk event dari producer yang evolve forward-compatible.

Policy:

  • ignore or capture unknown fields
  • tolerate new optional fields
  • preserve raw payload if needed
  • unknown enum strategy deliberate
  • schema version considered

Example:

public static ObjectMapper tolerantEventMapper() {
    return JsonMapper.builder()
        .addModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .build();
}

Tetapi jangan otomatis tolerant untuk semua event. Jika event adalah internal command-like message, strict bisa lebih aman.

4.3 Legacy Integration Mapper

Cocok untuk partner/provider lama.

Policy:

  • special date format
  • case-insensitive enum maybe
  • custom boolean code
  • XML/CSV/dataformat-specific mapping
  • raw field preservation

Keep it isolated.


5. Unknown Property Policy

Unknown property adalah compatibility decision.

{
  "orderId": "ORD-1",
  "status": "APPROVED",
  "newField": true
}

5.1 Fail Unknown

Good for:

  • command input
  • security-sensitive endpoint
  • public API with strict contract
  • admin action
  • financial/regulatory mutation
JsonMapper.builder()
    .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    .build();

Benefit:

  • catches typo
  • catches wrong client version
  • avoids false assumption that field was processed

Cost:

  • less forward-compatible
  • adding fields can break old consumers if used on consumer side

5.2 Ignore Unknown

Good for:

  • event consumer
  • read model ingestion
  • external provider payload where you only need subset
@JsonIgnoreProperties(ignoreUnknown = true)
public record ProviderEvent(
    String eventId,
    String eventType
) {}

Cost:

  • typo can be silently ignored
  • consumer may think data was used
  • hard to debug missing behavior

5.3 Capture Unknown

Best for audit/forward compatibility when unknown data matters.

public final class ProviderEvent {
    private String eventId;
    private String eventType;
    private final Map<String, JsonNode> extensions = new LinkedHashMap<>();

    @JsonAnySetter
    public void extension(String name, JsonNode value) {
        extensions.put(name, value);
    }

    @JsonAnyGetter
    public Map<String, JsonNode> extensions() {
        return extensions;
    }
}

6. Null and Inclusion Policy

Serialization inclusion controls what gets written.

@JsonInclude(JsonInclude.Include.NON_NULL)
public record CustomerResponse(
    String customerId,
    String displayName,
    String email
) {}

Global inclusion can be dangerous.

mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

If applied globally, response shape changes across all DTOs.

6.1 Decision Matrix

PolicyMeaningRisk
include nullsexplicit field is known but nullverbose, can expose nullable contract
omit nullsabsence means null/not availableambiguity between absent and null
omit emptyhides empty list/string/mapconsumer may not distinguish empty vs absent
per-field includemost explicitmore annotations

For stable contracts, prefer per-DTO/per-field policy over sweeping global policy unless your platform standard is explicit.


7. Date-Time Policy

For modern Java, configure Java Time explicitly.

ObjectMapper mapper = JsonMapper.builder()
    .addModule(new JavaTimeModule())
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .build();

Recommended boundary formats:

SemanticJava TypeWire Format
event/audit timestampInstantISO instant, e.g. 2026-06-29T03:00:00Z
provider timestamp with offsetOffsetDateTimeISO offset date-time
business dateLocalDateYYYY-MM-DD
billing periodYearMonthYYYY-MM or explicit object
durationDurationISO-8601 duration or numeric seconds by contract

Do not leave timestamp shape to framework default.


8. Enum Policy

Default enum serialization often uses enum name.

public enum PaymentStatus {
    WAITING_FOR_CUSTOMER,
    PAID,
    FAILED
}

Output:

"WAITING_FOR_CUSTOMER"

This couples Java constant name to external contract.

For external stable code:

public enum PaymentStatus {
    WAITING_FOR_CUSTOMER("WAITING"),
    PAID("PAID"),
    FAILED("FAILED");

    private final String code;

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

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

    @JsonCreator
    public static PaymentStatus fromCode(String raw) {
        if (raw == null) return null;
        return switch (raw.trim().toUpperCase(Locale.ROOT)) {
            case "WAITING" -> WAITING_FOR_CUSTOMER;
            case "PAID" -> PAID;
            case "FAILED" -> FAILED;
            default -> throw new IllegalArgumentException("Unknown payment status: " + raw);
        };
    }
}

8.1 Unknown Enum Decision

BoundaryRecommended Default
create/update requestreject unknown
event consumer from evolving producerpreserve raw or map to explicit unknown
read responseonly emit known contract values
legacy provider integrationnormalize with explicit converter

9. Naming Strategy

Global naming strategy:

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

This turns:

customerId

into:

customer_id

Useful when service standard is snake_case.

But beware:

  • changing global naming strategy is breaking
  • explicit @JsonProperty can override convention
  • external legacy contracts may not follow global convention
  • internal DTO and external DTO may require separate mapper/profile

For public API, make naming style part of contract governance.


10. Coercion Policy

Coercion means accepting one JSON type as another.

Examples:

{ "quantity": "10" }
{ "active": "true" }
{ "tags": "single-tag" }
{ "amount": 100.0 }

These may or may not be allowed depending on configuration and target type.

Production guidance:

  • strict for new APIs
  • tolerant only for legacy/provider integration
  • test coercion explicitly
  • do not rely on accidental defaults

Example test:

@Test
void quantity_rejectsStringWhenApiIsStrict() {
    String json = "{\"quantity\":\"10\"}";

    assertThatThrownBy(() -> mapper.readValue(json, CreateOrderRequest.class))
        .isInstanceOf(JsonMappingException.class);
}

If your platform allows numeric strings, document it as contract.


11. Primitive vs Wrapper Policy

Primitive fields default silently.

public record CreateOrderRequest(
    int quantity,
    boolean expedited
) {}

If input omits both fields:

{}

Java object can contain:

quantity = 0
expedited = false

This may hide absence.

For request DTOs, prefer wrappers with validation:

public record CreateOrderRequest(
    @NotNull @Min(1) Integer quantity,
    @NotNull Boolean expedited
) {}

Use primitive in domain when default is meaningful and object cannot represent invalid absence.


12. Records, Constructors, and Creators

Records are excellent DTO candidates.

public record CreateCustomerRequest(
    String customerId,
    String displayName
) {}

For custom construction:

public record CustomerCode(String value) {
    @JsonCreator
    public CustomerCode {
        if (value == null || !value.matches("[A-Z0-9]{8}")) {
            throw new IllegalArgumentException("invalid customer code");
        }
    }

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

Be careful not to overpack business validation into JSON creator if the type is used outside serialization. For value objects, constructor invariant is often good. For request-level policy, Jakarta Validation may be better.


13. Module Strategy

Recommended baseline for modern Java JSON mapper:

public static ObjectMapper baselineMapper() {
    return JsonMapper.builder()
        .addModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .build();
}

A richer platform mapper:

public static ObjectMapper platformMapper() {
    return JsonMapper.builder()
        .addModule(new JavaTimeModule())
        .addModule(new DomainJsonModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
        .build();
}

Module guidelines:

Module TypeUse For
Java/library datatype modulejava.time, Optional, specialized collections
domain modulestable value objects such as Money, CustomerCode
integration moduleprovider-specific oddities
test moduletest-only relaxed parsing or fixture behavior

Keep provider-specific modules away from core platform mapper.


14. Custom Serializer/Deserializer Governance

Custom serializers/deserializers are powerful but increase maintenance cost.

Use when:

  • value object has stable wire representation
  • third-party type needs special handling
  • legacy contract requires non-standard shape
  • sensitive field needs controlled redaction
  • performance requires specialized streaming

Avoid when:

  • DTO projection would solve it
  • MapStruct should perform semantic mapping
  • validation should reject invalid value
  • local annotation is enough

Example serializer for value object:

public final class CustomerCodeSerializer extends JsonSerializer<CustomerCode> {
    @Override
    public void serialize(
        CustomerCode value,
        JsonGenerator gen,
        SerializerProvider serializers
    ) throws IOException {
        gen.writeString(value.value());
    }
}

Example deserializer:

public final class CustomerCodeDeserializer extends JsonDeserializer<CustomerCode> {
    @Override
    public CustomerCode deserialize(
        JsonParser parser,
        DeserializationContext context
    ) throws IOException {
        return new CustomerCode(parser.getValueAsString());
    }
}

Register through module:

public final class DomainJsonModule extends SimpleModule {
    public DomainJsonModule() {
        addSerializer(CustomerCode.class, new CustomerCodeSerializer());
        addDeserializer(CustomerCode.class, new CustomerCodeDeserializer());
    }
}

15. Security Hardening

15.1 Avoid Unsafe Default Typing

Do not accept arbitrary Java class names from untrusted payloads.

Bad pattern conceptually:

{
  "@class": "some.runtime.ClassName",
  "value": "..."
}

Prefer explicit semantic discriminator:

{
  "type": "CARD",
  "last4": "1234"
}

And allowlist known subtypes.

15.2 Limit Payload Size Outside ObjectMapper

ObjectMapper is not your only defense. Enforce:

  • max request body size
  • max file size
  • stream processing for huge payload
  • depth constraints where possible
  • endpoint timeout
  • rate limit

15.3 Redaction

Do not rely only on @JsonIgnore if object is reused in multiple contexts.

Use output DTOs:

public record UserProfileResponse(
    String userId,
    String displayName
) {}

instead of serializing:

class User {
    String passwordHash;
    String resetToken;
    String internalRiskFlag;
}

16. Performance Configuration

Important performance rules:

  1. Reuse configured ObjectMapper.
  2. Reuse ObjectReader/ObjectWriter for hot paths.
  3. Avoid building mapper per request.
  4. Use streaming for large payloads.
  5. Avoid serializing large object graphs accidentally.
  6. Measure before adding performance modules.
  7. Keep payload shape small and purposeful.

Example hot-path writer:

public final class AuditEventWriter {
    private final ObjectWriter writer;

    public AuditEventWriter(ObjectMapper mapper) {
        this.writer = mapper.writerFor(AuditEvent.class);
    }

    public byte[] writeBytes(AuditEvent event) {
        try {
            return writer.writeValueAsBytes(event);
        } catch (JsonProcessingException e) {
            throw new AuditEncodingException(e);
        }
    }
}

Performance starts with contract shape. A bad graph serialized efficiently is still a bad payload.


17. Framework Integration Strategy

In Spring-like environments, there is often an auto-configured ObjectMapper.

Production approach:

  • know the framework defaults
  • define application standard explicitly
  • customize centrally
  • avoid creating unmanaged mappers in random classes
  • expose additional mappers only when profiles require it
  • test actual mapper used by controllers/message converters

Bad:

private final ObjectMapper mapper = new ObjectMapper();

inside a component when the application already has platform-configured mapper.

Better:

public SomeAdapter(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
}

But for legacy partner config, name it explicitly:

public SomePartnerAdapter(@Qualifier("partnerLegacyObjectMapper") ObjectMapper mapper) {
    this.mapper = mapper;
}

18. Configuration Drift

Configuration drift happens when different parts of the code use different mappers unintentionally.

Symptoms:

  • controller accepts payload that test mapper rejects
  • Kafka consumer ignores fields but REST endpoint fails
  • date format differs between services
  • enum casing differs between tests and production
  • local new ObjectMapper() fails on Java Time type

Controls:

ControlPurpose
central factory/configsingle source of truth
ban raw new ObjectMapper()avoid unmanaged behavior
codec classesboundary-level explicitness
fixture testscatch output/input drift
architecture testsenforce injection/factory usage
versioned shared moduleconsistent platform behavior

19. Versioned Contract Mappers

For long-lived APIs/events, versioning sometimes needs mapper separation.

public final class OrderEventV1Codec {
    private final ObjectReader reader;
    private final ObjectWriter writer;
}

public final class OrderEventV2Codec {
    private final ObjectReader reader;
    private final ObjectWriter writer;
}

Do not rely on one DTO with many optional fields forever.

If v1 and v2 have different field names, enum rules, or date format, explicit codecs are clearer.


20. Error Translation

Do not expose Jackson exception directly.

Example translator:

public final class JsonErrorTranslator {
    public ApiError translate(JsonProcessingException exception) {
        if (exception instanceof JsonParseException) {
            return ApiError.badRequest("MALFORMED_JSON", "Request body is not valid JSON.");
        }
        if (exception instanceof UnrecognizedPropertyException e) {
            return ApiError.badRequest(
                "UNKNOWN_FIELD",
                "Unknown field: " + e.getPropertyName()
            );
        }
        if (exception instanceof MismatchedInputException e) {
            return ApiError.badRequest(
                "INVALID_JSON_SHAPE",
                "Invalid value at: " + e.getPathReference()
            );
        }
        return ApiError.badRequest("INVALID_JSON", "Request body is invalid.");
    }
}

For public APIs, keep messages stable and safe.


21. Testing ObjectMapper Configuration

21.1 Baseline Feature Test

@Test
void apiMapper_rejectsUnknownProperties() {
    String json = """
        { "orderId": "ORD-1", "unknown": true }
        """;

    assertThatThrownBy(() -> apiMapper.readValue(json, OrderRequest.class))
        .isInstanceOf(UnrecognizedPropertyException.class);
}

21.2 Date-Time Test

@Test
void apiMapper_writesInstantAsIsoString() throws Exception {
    AuditEvent event = new AuditEvent(Instant.parse("2026-06-29T03:00:00Z"));

    String json = apiMapper.writeValueAsString(event);

    assertThat(json).contains("2026-06-29T03:00:00Z");
}

21.3 Inclusion Test

@Test
void response_includesOrOmitsNullAccordingToContract() throws Exception {
    CustomerResponse response = new CustomerResponse("CUS-1", null);

    String json = apiMapper.writeValueAsString(response);

    assertThatJson(json).isEqualTo("""
        {
          "customerId": "CUS-1",
          "displayName": null
        }
        """);
}

21.4 Enum Test

@Test
void paymentStatus_usesExternalCode() throws Exception {
    String json = apiMapper.writeValueAsString(PaymentStatus.WAITING_FOR_CUSTOMER);

    assertThat(json).isEqualTo("\"WAITING\"");
}

21.5 Drift Test for Managed Mapper

@Test
void noComponentCreatesRawObjectMapper() {
    // Use ArchUnit or similar architecture test in real project.
    // Rule: production code should not call new ObjectMapper() outside approved config package.
}

For a strict modern JSON API baseline:

public final class ApiObjectMapperConfig {
    private ApiObjectMapperConfig() {}

    public static ObjectMapper create() {
        return JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
            .build();
    }
}

Then add explicit policies as needed:

  • naming strategy
  • enum code strategy
  • domain value object module
  • inclusion standard
  • coercion standard
  • custom error translator

Do not blindly copy config from tutorials. Every feature is a contract decision.


23. Mapper Profile Example

public final class ObjectMappers {
    private ObjectMappers() {}

    public static ObjectMapper strictApi() {
        return JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .addModule(new DomainJsonModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
            .build();
    }

    public static ObjectMapper tolerantEvents() {
        return JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .addModule(new DomainJsonModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .build();
    }

    public static ObjectMapper legacyPartner() {
        return JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .addModule(new LegacyPartnerJsonModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .build();
    }
}

Notice: tolerant mapper is not “less correct”. It is correct for a different boundary.


24. Production Decision Matrix

DecisionStrict APIEvent ConsumerLegacy Partner
unknown fieldsfailignore/captureignore/capture
unknown enumrejectpreserve/unknowncustom normalize
datesISO strictISO/versionedprovider format
coercionminimalcontrolledoften needed
nullsexplicit validationtolerant if versionedprovider-specific
namingplatform standardschema standardpartner standard
custom moduledomain onlydomain + eventpartner module
error mappinguser-safe API errorsdead-letter/retry metadataintegration error category

25. Anti-Patterns

25.1 Mapper Mutation Near Callsite

mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES);
return mapper.readValue(json, Type.class);

Use separate mapper or reader instead.

25.2 Raw new ObjectMapper() Everywhere

Creates inconsistent behavior.

25.3 Global Config for Local Problem

Do not change global inclusion/naming/coercion because one DTO needs special behavior.

25.4 Business Logic in Deserializer

Deserializer should parse/bind. Domain decisions belong in domain layer or explicit mapper/service.

25.5 Unreviewed Custom Modules

A module can change behavior for an entire type everywhere. Treat it as platform code.

25.6 Contract by Accident

If output shape is only tested manually, it is accidental.


26. Practice Drill

Design three mappers:

  1. strictApiMapper
  2. tolerantEventMapper
  3. legacyProviderMapper

For this DTO:

public record CustomerUpdateRequest(
    String customerId,
    String displayName,
    LocalDate birthDate,
    CustomerStatus status,
    Boolean marketingConsent
) {}

Write tests for:

  • unknown field
  • missing marketingConsent
  • lowercase enum
  • unknown enum
  • date as ISO string
  • date as numeric timestamp
  • null primitive/wrapper behavior
  • extra provider field captured or ignored

Then decide which behavior belongs to which boundary.


27. Master Checklist

Before shipping ObjectMapper config:

  • Is mapper built once and reused?
  • Is mutation after first use avoided?
  • Are mapper profiles named by boundary?
  • Are Java Time types tested?
  • Are unknown property rules explicit?
  • Are null inclusion rules explicit?
  • Are enum rules explicit?
  • Are coercion rules explicit?
  • Are custom modules reviewed as platform behavior?
  • Are framework auto-configured mappers aligned?
  • Are ObjectReader/ObjectWriter used for hot/important boundaries?
  • Are Jackson errors translated into stable application errors?
  • Are contract fixtures checked into tests?
  • Are raw new ObjectMapper() calls banned outside config/test fixtures?
  • Is Jackson 2/3 migration risk isolated behind config/codecs?

28. Summary

ObjectMapper production configuration is not a cosmetic setup step. It is a boundary governance mechanism.

Core rules:

  1. Configure once, then reuse.
  2. Treat mapper as infrastructure.
  3. Use profile-specific mappers for different boundary policies.
  4. Use ObjectReader and ObjectWriter for stable typed operations.
  5. Make unknown fields, nulls, enum, coercion, date/time, and naming decisions explicit.
  6. Keep legacy/provider oddities isolated.
  7. Translate Jackson errors into stable application errors.
  8. Test the mapper configuration itself.
  9. Avoid global changes for local problems.
  10. Lock JSON contract with fixtures.

Next part focuses on the JSON tree model: JsonNode, partial reads, dynamic payloads, extension fields, safe traversal, and patch-like workflows.


References

Lesson Recap

You just completed lesson 10 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.