1
0

Jackson 2 to 3 migration companion code

Three Maven modules - jackson2-before (2.22.1), jackson3-after (3.2.1) and a
coexistence module with BOTH majors on one classpath - so every claim in the two
migration guides is executed rather than asserted. Paired class names make the
before/after outputs directly diffable via run-all.sh.

Confirms the guides on wire-format equivalence (10-case suite, zero mismatches),
classpath coexistence and the collapse of four artifacts into one. Corrects nine
points, including that enableDefaultTyping() is still present in Jackson 2.22.1
rather than removed in 2.16, and that the published "after" mapper snippet does
not compile.
This commit is contained in:
2026-08-04 23:12:25 +05:30
commit f0a7053fc9
43 changed files with 1665 additions and 0 deletions

61
coexistence/pom.xml Normal file
View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ankurm</groupId>
<artifactId>jackson2-to-3-migration</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>coexistence</artifactId>
<name>coexistence</name>
<description>Both major versions on ONE classpath — the claim, tested</description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>tools.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson3.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson2.version}</version>
</dependency>
<!-- Jackson 2 still needs its java.time module even here; Jackson 3's built-in
support does not apply to the Jackson 2 mapper. Nothing is shared between
them except jackson-annotations. -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson2.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>${jackson2.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,59 @@
package com.ankurm.migration.coexist;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDate;
/**
* Post: https://ankurm.com/jackson-3-vs-jackson-2/
* FAQ: "Can Jackson 3 and Jackson 2 be on the same classpath?"
*
* The posts assert that the two majors coexist because the packages differ. This
* module's pom.xml puts BOTH jackson-databind artifacts on one classpath and this
* class uses both at once, so the claim is executed rather than repeated.
*
* Watch the DTO: it is annotated once, with @JsonProperty imported from
* com.fasterxml.jackson.annotation, and BOTH mappers honour it. That is the whole
* reason jackson-annotations deliberately kept the old group ID and package.
*/
public class S06BothOnOneClasspath {
/** One DTO, one set of annotations, read by both Jackson majors. */
public record Booking(
@JsonProperty("booking_id") Long id,
@JsonProperty("travel_date") LocalDate travelDate) { }
public static void main(String[] args) throws Exception {
// --- Jackson 2 ---
var mapper2 = new com.fasterxml.jackson.databind.ObjectMapper();
mapper2.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
mapper2.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// --- Jackson 3 ---
var mapper3 = tools.jackson.databind.json.JsonMapper.builder().build();
Booking booking = new Booking(1L, LocalDate.of(2026, 9, 15));
String json2 = mapper2.writeValueAsString(booking);
String json3 = mapper3.writeValueAsString(booking);
System.out.println("jackson 2 class : " + mapper2.getClass().getName());
System.out.println("jackson 3 class : " + mapper3.getClass().getName());
System.out.println();
System.out.println("jackson 2 output: " + json2);
System.out.println("jackson 3 output: " + json3);
System.out.println("wire-compatible : " + json2.equals(json3));
System.out.println();
// Cross-read: each major reads the other's output.
System.out.println("3 reads 2's JSON: " + mapper3.readValue(json2, Booking.class));
System.out.println("2 reads 3's JSON: " + mapper2.readValue(json3, Booking.class));
System.out.println();
// And the annotation both of them obeyed came from a single shared artifact.
System.out.println("annotation from : "
+ JsonProperty.class.getProtectionDomain().getCodeSource().getLocation()
.toString().replaceAll(".*/", ""));
System.out.println(" <- one jackson-annotations jar, used by both majors");
}
}

View File

@@ -0,0 +1,83 @@
package com.ankurm.migration.coexist;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* The regression suite the comparison post's last AI prompt asks for, actually written.
*
* Post: https://ankurm.com/jackson-3-vs-jackson-2/
* FAQ: "Is Jackson 3 backward compatible with Jackson 2 at the serialised JSON level?"
* — the post answers "Yes ... identical for standard types". This checks it.
*
* Both mappers are configured as equivalently as each API allows, then fed the same
* values. Every mismatch is printed. Run this before switching a service whose output
* is cached, signed, or consumed downstream.
*/
public class S08WireFormatEquivalence {
public record Product(Long id, String name, BigDecimal price) { }
public record Timestamps(LocalDate day, LocalDateTime at, Instant instant) { }
public record WithOptional(String name, Optional<String> nickname) { }
@JsonInclude(JsonInclude.Include.NON_NULL)
public record Sparse(String present, String absent) { }
public record Renamed(@JsonProperty("order_id") Long orderId) { }
public record Formatted(@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd/MM/yyyy") LocalDate d) { }
public static void main(String[] args) throws Exception {
var mapper2 = new com.fasterxml.jackson.databind.ObjectMapper()
.registerModule(new JavaTimeModule())
.registerModule(new Jdk8Module())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
var mapper3 = tools.jackson.databind.json.JsonMapper.builder().build();
record Case(String label, Object value) { }
List<Case> cases = List.of(
new Case("plain record", new Product(1L, "Keyboard", new BigDecimal("79.99"))),
new Case("nested list", List.of(new Product(1L, "A", BigDecimal.ONE),
new Product(2L, "B", BigDecimal.TEN))),
new Case("map", Map.of("k", new Product(3L, "C", BigDecimal.ZERO))),
new Case("java.time", new Timestamps(LocalDate.of(2026, 9, 15),
LocalDateTime.of(2026, 9, 15, 10, 30),
Instant.parse("2026-09-15T10:30:00Z"))),
new Case("Optional present", new WithOptional("Alice", Optional.of("Ali"))),
new Case("Optional empty", new WithOptional("Bob", Optional.empty())),
new Case("NON_NULL", new Sparse("here", null)),
new Case("@JsonProperty", new Renamed(1001L)),
new Case("@JsonFormat", new Formatted(LocalDate.of(2026, 9, 15))),
new Case("nulls and empties", Map.of("list", List.of(), "str", "")));
int mismatches = 0;
for (Case c : cases) {
String j2 = mapper2.writeValueAsString(c.value());
String j3 = mapper3.writeValueAsString(c.value());
boolean same = j2.equals(j3);
if (!same) mismatches++;
System.out.printf("%-18s %s%n", c.label(), same ? "identical" : "DIFFERS");
if (!same) {
System.out.println(" jackson2: " + j2);
System.out.println(" jackson3: " + j3);
}
}
System.out.println();
System.out.println("cases : " + cases.size());
System.out.println("mismatches: " + mismatches);
System.out.println(mismatches == 0
? "Wire format is identical across both majors for these cases."
: "Wire format differs — review each DIFFERS case before upgrading.");
}
}