Series MapLesson 17 / 32
Build CoreOrdered learning track

Learn Java Data Mapper Json Xml Validation Part 017 Jackson 2 To 3 Migration

13 min read2509 words
PrevNext
Lesson 1732 lesson track0718 Build Core

title: Learn Java Data Mapper, JSON/XML Processing & Validation - Part 017 description: Jackson 2.x to 3.x migration strategy: compatibility risk, namespace/package changes, dependency alignment, ObjectMapper policy, custom codec impact, framework support, and safe rollout. series: learn-java-data-mapper-json-xml-validation seriesTitle: Learn Java Data Mapper, JSON/XML Processing & Validation order: 17 partTitle: Jackson 2.x to 3.x: Package Changes, Compatibility Risk, Migration Strategy tags:

  • java
  • jackson
  • jackson-3
  • migration
  • objectmapper
  • serialization
  • deserialization
  • compatibility
  • contract date: 2026-06-29

Part 017 — Jackson 2.x to 3.x: Package Changes, Compatibility Risk, Migration Strategy

Target skill: mampu merencanakan migrasi Jackson 2.x ke 3.x sebagai perubahan platform serialization, bukan sekadar bump dependency.

Jackson 3 adalah major version line. Major version berarti ada risiko API incompatibility, perubahan dependency coordinates/package, perubahan default behavior, dan dampak ke framework integration. Untuk sistem enterprise, perubahan serialization library bisa memengaruhi:

  • API request parsing
  • API response shape
  • event producer/consumer compatibility
  • audit payload
  • import/export
  • custom serializers/deserializers
  • polymorphic type handling
  • security posture
  • test fixtures
  • generated clients
  • framework auto-configuration

Mental model:

Jackson migration is a contract migration. Treat it like changing a boundary runtime, not like updating a utility library.


1. Kaufman Deconstruction

Migrasi ini kita pecah menjadi subskill praktis:

SubskillKemampuan
Inventory usageMenemukan semua ObjectMapper, modules, custom codec, annotations, converters
Identify boundary profilesMemisahkan API/event/audit/import/export mapper
Align dependenciesMenghindari mix Jackson 2.x/3.x yang tidak disengaja
Compile migrationMenangani package/API changes
Contract migrationMembandingkan serialized/deserialized behavior
Custom codec migrationMemperbaiki serializer/deserializer/module yang pakai API berubah
Framework migrationMemastikan Spring/Quarkus/Micronaut/Jersey/etc mendukung versi target
Security reviewMengevaluasi polymorphism/default typing changes
Performance reviewMengukur throughput/allocation after upgrade
Rollout strategyStaged rollout, compatibility bridge, rollback plan

Latihan utama:

  1. Buat inventory semua mapper.
  2. Tambahkan golden contract tests.
  3. Jalankan upgrade branch.
  4. Perbaiki compile errors.
  5. Bandingkan JSON output.
  6. Jalankan integration tests producer/consumer.
  7. Rollout satu mapper profile/service dulu.
  8. Monitor error parsing, unknown field, invalid type, latency.

2. Why This Migration Is Not Trivial

Jackson sering berada di jalur kritis:

Jika migration mengubah parsing atau output shape, consumer bisa rusak walaupun business code tidak berubah.

Contoh risiko:

AreaRisiko
date-timeoutput timestamp berubah dari string ke numeric/array
enumunknown enum behavior berubah
null inclusionfield yang dulu ada sekarang hilang atau sebaliknya
constructor bindingimmutable DTO gagal deserialize
polymorphismdiscriminator/type handling berubah
custom codeccompile/runtime error
modulesmodule belum tersedia/berubah
annotationsbehavior annotation berubah/unsupported edge
frameworkHTTP converter memakai mapper berbeda
dependency graphlibrary masih membawa Jackson 2.x

3. Migration Principle

Jangan mulai dengan:

Change version -> fix compile -> deploy

Mulai dengan:

Define current contract -> upgrade -> prove contract remains intentional

Recommended flow:


4. Inventory Checklist

Cari semua penggunaan berikut:

ObjectMapper
JsonMapper
ObjectReader
ObjectWriter
JsonParser
JsonGenerator
JsonNode
ObjectNode
ArrayNode
JsonSerializer
JsonDeserializer
StdSerializer
StdDeserializer
SimpleModule
Module
@JsonTypeInfo
@JsonSubTypes
@JsonCreator
@JsonProperty
@JsonFormat
@JsonInclude
@JsonAlias
@JsonIgnoreProperties
activateDefaultTyping
enableDefaultTyping
registerSubtypes
findAndAddModules

Command examples:

rg "new ObjectMapper|JsonMapper|ObjectReader|ObjectWriter" src test
rg "JsonSerializer|JsonDeserializer|StdSerializer|StdDeserializer|SimpleModule" src test
rg "@JsonTypeInfo|@JsonSubTypes|activateDefaultTyping|enableDefaultTyping" src test
rg "findAndAddModules|registerModule|registerSubtypes" src test

Inventory table:

ItemLocationBoundaryRisk
strictApiMapperJsonConfigREST requesthigh
eventMapperKafkaConfigeventshigh
auditMapperAuditServiceauditmedium
MoneyDeserializerjson/modulevalue objecthigh
@JsonTypeInfopayment DTOpolymorphic requesthigh
new ObjectMapper()test utiltestsmedium

5. Boundary Mapper Profiles

Before migrating, classify mapper usage.

ProfileRequired tests
strict API requestunknown field, invalid type, null, enum, date
public API responsegolden output, null inclusion, date format
event producerschema/fixture compatibility
event consumerold/new event fixtures, unknown field/type
importlarge file, invalid row, error path
exportgolden output, ordering if needed, memory
auditraw/sanitized preservation
provider integrationlegacy quirks, alias, custom date

Do not let a hidden new ObjectMapper() escape this classification.


6. Dependency Alignment

Jackson has multiple artifacts. Keep versions aligned.

Maven with BOM:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>tools.jackson</groupId>
      <artifactId>jackson-bom</artifactId>
      <version>${jackson.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Gradle:

dependencies {
    implementation(platform("tools.jackson:jackson-bom:$jacksonVersion"))
    implementation("tools.jackson.core:jackson-databind")
}

During migration, run dependency tree:

mvn dependency:tree | rg "jackson"

or:

./gradlew dependencies --configuration runtimeClasspath | grep jackson

Look for mixed 2.x and 3.x.

6.1 Why Version Soup Is Dangerous

If one library pulls jackson-databind 2.x while app uses 3.x modules, behavior can fail at runtime or compile inconsistently.

Use dependency constraints/exclusions intentionally.


7. Package and Coordinate Awareness

Jackson 3 introduces significant package/coordinate changes for core components. Do not assume imports remain identical.

Expect migration work around imports like:

import com.fasterxml.jackson.databind.ObjectMapper;

Depending artifact/version line, new imports may use different package names for Jackson 3 core/databind APIs.

Migration checklist:

  • update dependency coordinates
  • update imports
  • update module artifact names
  • update framework integration
  • update custom serializer/deserializer imports
  • update test utilities
  • update code generation/templates if any
  • update documentation snippets

Do not use search/replace blindly. Compile and test by boundary.


8. Golden Contract Tests Before Upgrade

Before upgrade, freeze intended behavior.

8.1 Response Output

@Test
void customerResponse_contract() throws Exception {
    CustomerResponse response = new CustomerResponse(
        "CUS-001",
        "Ana",
        Instant.parse("2026-06-29T03:00:00Z")
    );

    assertThatJson(objectMapper.writeValueAsString(response)).isEqualTo("""
    {
      "customerId": "CUS-001",
      "fullName": "Ana",
      "createdAt": "2026-06-29T03:00:00Z"
    }
    """);
}

8.2 Request Input

@Test
void createPaymentRequest_acceptsCanonicalInput() throws Exception {
    CreatePaymentRequest request = objectMapper.readValue("""
    {
      "requestId": "REQ-001",
      "amount": {
        "amount": "100.00",
        "currency": "IDR"
      }
    }
    """, CreatePaymentRequest.class);

    assertThat(request.requestId()).isEqualTo("REQ-001");
}

8.3 Invalid Input

@Test
void createPaymentRequest_rejectsUnknownField() {
    assertThatThrownBy(() -> objectMapper.readValue("""
    {
      "requestId": "REQ-001",
      "unknown": "x"
    }
    """, CreatePaymentRequest.class))
    .isInstanceOf(JsonProcessingException.class);
}

Golden tests make migration measurable.


9. Contract Diff Strategy

For important DTOs/events, serialize representative objects with old mapper and new mapper.

String oldJson = oldMapper.writeValueAsString(sample);
String newJson = newMapper.writeValueAsString(sample);

assertThatJson(newJson).isEqualTo(oldJson);

If different, classify:

DifferenceAction
intended improvementdocument, version if breaking
accidental behavior changefix config/module/annotation
harmless ordering diffignore order unless canonical/signature
date format diffusually fix
null inclusion diffreview contract
enum casing diffreview
unknown field behavior difffix profile

Do not auto-accept all diffs.


10. Custom Serializer/Deserializer Migration

Custom codecs are high-risk because they touch Jackson APIs directly.

Inventory classes:

extends JsonSerializer<?>
extends JsonDeserializer<?>
extends StdSerializer<?>
extends StdDeserializer<?>
implements ContextualSerializer
implements ContextualDeserializer

Migration tasks:

  • update imports
  • check method signatures
  • check exception handling
  • check JsonParser/JsonGenerator package
  • check ObjectCodec usage
  • check SimpleModule APIs
  • check contextual serializer/deserializer behavior
  • run valid/invalid/round-trip tests

Example test set for every codec:

TestPurpose
valid serializeoutput shape stable
valid deserializeinput accepted
invalid tokenrejects wrong shape
nullnull behavior explicit
unknown fieldstrict/tolerant policy
round-tripif applicable
secret handlingno leak
error messagesupportable

11. Module Migration

List registered modules:

JsonMapper.builder()
    .addModule(new JavaTimeModule())
    .addModule(new ParameterNamesModule())
    .addModule(new BoundaryValueModule())
    .addModule(new AfterburnerModule())
    .build();

For each module:

ModuleMigration question
Java Timeartifact available? behavior same?
JDK8 datatypeOptional behavior same?
Parameter namesstill needed? artifact available?
Afterburnersupported? beneficial?
Blackbirdsupported? beneficial?
custom modulescompile? APIs changed?
dataformat XML/YAML/CSVversion available? behavior same?
framework modulesframework support ready?

Do not assume every Jackson 2 module has same Jackson 3 replacement behavior.


12. Framework Integration

Frameworks manage HTTP serialization differently. For example, a web framework might auto-configure:

  • HTTP message converters
  • mapper customizers
  • Java Time module
  • unknown property policy
  • Kotlin/record/parameter modules
  • problem detail serialization
  • test client mapper
  • object mapper bean ordering

Migration check:

@SpringBootTest
class RealHttpObjectMapperTest {

    @Autowired ObjectMapper mapper;

    @Test
    void frameworkMapper_hasExpectedDatePolicy() throws Exception {
        String json = mapper.writeValueAsString(
            new EventResponse(Instant.parse("2026-06-29T03:00:00Z"))
        );

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

Also test actual HTTP endpoint, not only mapper bean.

mockMvc.perform(get("/customers/CUS-001"))
    .andExpect(jsonPath("$.createdAt").value("2026-06-29T03:00:00Z"));

13. Polymorphism Migration

Polymorphic behavior is security-sensitive.

Search:

rg "@JsonTypeInfo|activateDefaultTyping|enableDefaultTyping|registerSubtypes|NamedType" src test

Review:

  • Are type ids logical names?
  • Are class names exposed?
  • Is subtype set explicit?
  • Is default typing used?
  • Is PolymorphicTypeValidator used where needed?
  • Are unknown subtypes tested?
  • Are sealed hierarchies used?
  • Is event routing better as manual dispatcher?

Migration should be an opportunity to remove broad default typing.

Bad:

activateDefaultTyping(
    permissiveValidator,
    ObjectMapper.DefaultTyping.NON_FINAL,
    JsonTypeInfo.As.PROPERTY
)

Better:

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type"
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = CardPaymentMethod.class, name = "CARD")
})
public sealed interface PaymentMethod permits CardPaymentMethod {}

14. Deserialization Coercion

Migration can reveal accidental coercion.

Examples:

{ "quantity": "10" }
{ "active": "true" }
{ "status": 1 }
{ "amount": 100.00 }

Decide:

FieldShould string be accepted?
quantityusually no for strict API
booleanusually no for JSON API
money amountmaybe string only
enumstring logical code
provider legacymaybe yes with boundary mapper

Add tests:

@Test
void strictApi_rejectsStringForInteger() {
    assertThatThrownBy(() -> strictMapper.readValue("""
    { "quantity": "10" }
    """, QuantityRequest.class))
    .isInstanceOf(JsonProcessingException.class);
}

Do not let upgrade silently broaden or narrow coercion without decision.


15. Null and Absence

Test these across migration:

{}
{ "name": null }
{ "name": "" }
{ "items": [] }

Contract matrix:

CaseExpected
missing required fieldvalidation error or mapping error
explicit null required fieldvalidation/mapping error
empty stringvalidation error if @NotBlank
empty arrayallowed/rejected based on @NotEmpty
omitted nullable response fieldstable inclusion policy

If migration changes null inclusion, that is contract-impacting.


16. Date-Time Migration

Date-time output must be protected by golden tests.

Required checks:

  • Instant serializes as ISO string
  • OffsetDateTime preserves offset if required
  • LocalDate stays yyyy-MM-dd
  • no accidental timestamp arrays/numbers
  • no locale-specific format unless legacy
  • deserialization accepts intended format only

Test:

@Test
void localDate_contract() throws Exception {
    ReportRequest request = mapper.readValue("""
    { "businessDate": "2026-06-29" }
    """, ReportRequest.class);

    assertThat(request.businessDate()).isEqualTo(LocalDate.of(2026, 6, 29));
}

17. Error Model Migration

Raw Jackson exception messages may change across versions. Do not expose them directly as API contract.

Instead map errors to stable application error codes.

Bad API response:

{
  "message": "Cannot deserialize value of type..."
}

Better:

{
  "code": "INVALID_FIELD_TYPE",
  "field": "amount",
  "message": "amount must be a decimal string"
}

Migration task:

  • ensure exception mapper still catches new exception classes/types
  • ensure field path extraction still works
  • ensure raw messages are not exposed
  • test malformed JSON, invalid type, unknown property, missing subtype

18. Performance Migration

Do not assume Jackson 3 is faster/slower for your workload. Measure.

Benchmark scenarios:

ScenarioMeasure
small REST DTOlatency/allocation
event payloadthroughput
large importrecords/sec and heap
large exportthroughput and memory
custom codecCPU and allocation
polymorphic payloadoverhead
module profilecold/warm behavior

Use production-like payloads. Microbenchmarks are useful, but boundary integration measurement matters more.


19. Rollout Strategy

19.1 Library First or App First?

If you own shared libraries with Jackson types in public API, migrate carefully.

Options:

StrategyProsCons
app-by-app migrationisolated rolloutduplicated work
platform library migrationconsistentcan block many teams
dual support layergradualcomplexity
compatibility branchsafermaintenance cost

19.2 Staged Deployment

For event systems:

  1. Upgrade consumer in tolerant mode.
  2. Verify it accepts old producer payloads.
  3. Upgrade producer.
  4. Compare emitted events.
  5. Monitor consumer failures.
  6. Remove compatibility bridge later.

For HTTP APIs:

  1. Upgrade internal endpoints first.
  2. Run contract tests.
  3. Deploy canary.
  4. Monitor 4xx parsing errors and 5xx serialization errors.
  5. Compare response payload samples.
  6. Roll forward or rollback.

20. Observability During Migration

Add temporary metrics:

MetricPurpose
deserialization failures by endpoint/eventTypedetect parsing regressions
unknown property failuresstrictness regressions
invalid type failurescoercion changes
unknown subtype failurespolymorphism issues
response serialization failuresoutput DTO issues
payload size distributioninclusion/shape changes
p95/p99 serialization latencyperformance regression
mapper profile useddetect wrong mapper

Logs should include:

  • correlation id
  • endpoint/event type
  • mapper profile
  • stable error code
  • field path if safe
  • not raw secrets

21. Migration Anti-Patterns

21.1 Compile Success Means Migration Success

Wrong. Contract behavior may change without compile errors.

21.2 New Mapper in Tests

Tests pass with mapper that production does not use.

21.3 Ignoring Event Consumers

Producer output change can break downstream services outside your repo.

21.4 Enabling Tolerance Everywhere

Disabling unknown property failures globally hides command API errors.

21.5 Keeping Broad Default Typing

Major migration is the right time to reduce risky polymorphism.

21.6 No Rollback Plan

Serialization regression can affect every request. Plan rollback.


22. Migration Checklist

22.1 Before Upgrade

  • Inventory all Jackson usage.
  • Identify mapper profiles.
  • Align dependencies through BOM/dependency management.
  • Add golden serialization tests.
  • Add deserialization fixtures.
  • Add invalid input tests.
  • Add polymorphism tests.
  • Add custom codec tests.
  • Add framework HTTP tests.
  • Add event producer/consumer compatibility tests.

22.2 During Upgrade

  • Update dependency coordinates.
  • Update imports.
  • Replace removed/deprecated APIs.
  • Verify module availability.
  • Fix custom codecs.
  • Fix subtype registration.
  • Re-run all contract tests.
  • Review every JSON diff.
  • Benchmark critical paths.

22.3 After Upgrade

  • Deploy canary.
  • Monitor parsing/serialization failures.
  • Compare response/event samples.
  • Validate payload size distribution.
  • Keep rollback path.
  • Remove temporary compatibility code only after evidence.
  • Update internal docs/snippets.

23. Mini Case Study: Payment Service Migration

23.1 Current State

Payment service has:

  • REST request mapper
  • REST response mapper
  • Kafka event mapper
  • provider webhook mapper
  • custom Money serializer/deserializer
  • polymorphic PaymentMethod
  • Java Time module
  • @JsonAlias for legacy field names

23.2 Migration Plan

23.3 Required Fixtures

create-payment-valid-card.json
create-payment-valid-bank-transfer.json
create-payment-unknown-method.json
create-payment-invalid-money.json
payment-approved-event-v1.json
payment-approved-event-with-extra-field.json
provider-webhook-legacy-date.json
provider-webhook-unknown-status.json

23.4 Success Criteria

  • response JSON unchanged unless explicitly approved
  • old event fixtures still deserialize
  • unknown event field policy unchanged
  • unknown payment method rejected with stable error code
  • money output remains decimal string
  • date-time remains ISO string
  • no raw class names in polymorphic JSON
  • latency and allocation not worse beyond accepted threshold

24. Practice Drill

Take one service/module you own and produce a Jackson migration inventory:

  1. List all mappers.
  2. Classify mapper profiles.
  3. List all modules.
  4. List all custom serializers/deserializers.
  5. List all polymorphic DTOs.
  6. List all DTOs with @JsonFormat, @JsonInclude, @JsonAlias.
  7. Create five golden output fixtures.
  8. Create five invalid input fixtures.
  9. Define rollout order.
  10. Define rollback trigger metrics.

25. Summary

Jackson 2.x to 3.x migration is not just dependency work.

Mental model:

If Jackson sits on a boundary, migration is boundary migration.

Rules:

  1. Inventory before upgrading.
  2. Align dependencies.
  3. Use mapper profiles.
  4. Add golden contract tests before migration.
  5. Treat JSON diffs as design decisions.
  6. Review custom codecs carefully.
  7. Review polymorphism and default typing as security-sensitive.
  8. Test framework-managed mapper, not only local mapper.
  9. Benchmark critical paths.
  10. Roll out gradually with observability and rollback.

Next part starts XML: DOM, SAX, StAX, JAXB/Jakarta XML Binding, schema, namespaces, and how XML processing differs structurally from JSON processing.


References

Lesson Recap

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