From 1a9ae3f701f1d05e1b25e72efb32f0d2cfea85f6 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Fri, 1 Aug 2025 00:02:08 +0900 Subject: [PATCH 1/6] Reproduce #5238 --- .../records/JsonIdentityOnRecord5328Test.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java new file mode 100644 index 0000000000..83babffaed --- /dev/null +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java @@ -0,0 +1,82 @@ +package com.fasterxml.jackson.databind.records; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class JsonIdentityOnRecord5328Test { + + private final ObjectMapper MAPPER = new ObjectMapper(); + + // ✅ Working test with POJO + @Test + void testIdentityWithPojo() throws Exception { + ThingPojo t1 = new ThingPojo(1, "a"); + ThingPojo t2 = new ThingPojo(2, "b"); + + ExamplePojo input = new ExamplePojo(List.of(t1, t2), t2); + + String json = MAPPER.writeValueAsString(input); + ExamplePojo result = MAPPER.readValue(json, ExamplePojo.class); + + assertEquals(input.selected.id, result.selected.id); + assertEquals(input.selected.name, result.selected.name); + } + + // ❌ Failing test with record + @Test + void testIdentityWithRecord() throws Exception { + ThingRecord t1 = new ThingRecord(1, "a"); + ThingRecord t2 = new ThingRecord(2, "b"); + + ExampleRecord input = new ExampleRecord(List.of(t1, t2), t2); + + String json = MAPPER.writeValueAsString(input); + + Exception ex = assertThrows(Exception.class, () -> + MAPPER.readValue(json, ExampleRecord.class)); + + System.out.println("Expected failure:"); + ex.printStackTrace(); + } + + // Record-based data + record ExampleRecord(List allThings, ThingRecord selected) {} + + @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") + record ThingRecord(int id, String name) {} + + // POJO-based data + static class ExamplePojo { + public List allThings; + public ThingPojo selected; + + @JsonCreator + public ExamplePojo( + @JsonProperty("allThings") List allThings, + @JsonProperty("selected") ThingPojo selected) { + this.allThings = allThings; + this.selected = selected; + } + } + + @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") + static class ThingPojo { + public final int id; + public final String name; + + @JsonCreator + public ThingPojo(@JsonProperty("id") int id, @JsonProperty("name") String name) { + this.id = id; + this.name = name; + } + } +} \ No newline at end of file From 3ea8ccd690efcda48ab19b53d0b11d4097c589c0 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Fri, 1 Aug 2025 00:03:50 +0900 Subject: [PATCH 2/6] Add comment --- .../databind/records/JsonIdentityOnRecord5328Test.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java index 83babffaed..459c4ad650 100644 --- a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java @@ -12,7 +12,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -public class JsonIdentityOnRecord5328Test { +// [databind#5328] immutable classes with @JsonIdentityInfo can be deserialized; records cannot +public class JsonIdentityOnRecord5328Test +{ private final ObjectMapper MAPPER = new ObjectMapper(); From 04fb870339904d3f213b634efa49520fcf033293 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 2 Aug 2025 23:08:01 +0900 Subject: [PATCH 3/6] Finzlie #5238 --- .../deser/impl/PropertyValueBuffer.java | 7 +- ...java => JsonIdentityOnRecord5238Test.java} | 93 +++++++++---------- 2 files changed, 52 insertions(+), 48 deletions(-) rename src/test-jdk17/java/com/fasterxml/jackson/databind/records/{JsonIdentityOnRecord5328Test.java => JsonIdentityOnRecord5238Test.java} (69%) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index d58c684775..019d7a7feb 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -335,7 +335,12 @@ public Object handleIdValue(final DeserializationContext ctxt, Object bean) thro // also: may need to set a property value as well SettableBeanProperty idProp = _objectIdReader.idProperty; if (idProp != null) { - return idProp.setAndReturn(bean, _idValue); + // [databind#5328] Records do not have setters, skip...to set id value + if (bean.getClass().isRecord()) { + return bean; + } else { + return idProp.setAndReturn(bean, _idValue); + } } } else { // 07-Jun-2016, tatu: Trying to improve error messaging here... diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5238Test.java similarity index 69% rename from src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java rename to src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5238Test.java index 459c4ad650..2e3512236b 100644 --- a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5328Test.java +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5238Test.java @@ -1,60 +1,24 @@ package com.fasterxml.jackson.databind.records; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIdentityInfo; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.ObjectIdGenerators; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; - import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -// [databind#5328] immutable classes with @JsonIdentityInfo can be deserialized; records cannot -public class JsonIdentityOnRecord5328Test -{ - - private final ObjectMapper MAPPER = new ObjectMapper(); - - // ✅ Working test with POJO - @Test - void testIdentityWithPojo() throws Exception { - ThingPojo t1 = new ThingPojo(1, "a"); - ThingPojo t2 = new ThingPojo(2, "b"); - - ExamplePojo input = new ExamplePojo(List.of(t1, t2), t2); - - String json = MAPPER.writeValueAsString(input); - ExamplePojo result = MAPPER.readValue(json, ExamplePojo.class); - - assertEquals(input.selected.id, result.selected.id); - assertEquals(input.selected.name, result.selected.name); - } - - // ❌ Failing test with record - @Test - void testIdentityWithRecord() throws Exception { - ThingRecord t1 = new ThingRecord(1, "a"); - ThingRecord t2 = new ThingRecord(2, "b"); - - ExampleRecord input = new ExampleRecord(List.of(t1, t2), t2); - - String json = MAPPER.writeValueAsString(input); +import org.junit.jupiter.api.Test; - Exception ex = assertThrows(Exception.class, () -> - MAPPER.readValue(json, ExampleRecord.class)); +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; - System.out.println("Expected failure:"); - ex.printStackTrace(); - } +import static org.junit.jupiter.api.Assertions.*; +// [databind#5238] immutable classes with @JsonIdentityInfo can be deserialized; records cannot +public class JsonIdentityOnRecord5238Test + extends DatabindTestUtil +{ // Record-based data - record ExampleRecord(List allThings, ThingRecord selected) {} + record ExampleRecord(List allThings, ThingRecord selected) { } @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") - record ThingRecord(int id, String name) {} + record ThingRecord(int id, String name) { } // POJO-based data static class ExamplePojo { @@ -81,4 +45,39 @@ public ThingPojo(@JsonProperty("id") int id, @JsonProperty("name") String name) this.name = name; } } + + private final ObjectMapper MAPPER = newJsonMapper(); + + @Test + void testIdentityWithPojo() throws Exception { + ThingPojo t1 = new ThingPojo(1, "a"); + ThingPojo t2 = new ThingPojo(2, "b"); + ExamplePojo input = new ExamplePojo(List.of(t1, t2), t2); + + String json = MAPPER.writeValueAsString(input); + + // Then : Check deserialization result, values + ExamplePojo result = MAPPER.readValue(json, ExamplePojo.class); + assertEquals(input.allThings.size(), result.allThings.size()); + assertEquals(input.selected.id, result.selected.id); + assertEquals(input.selected.name, result.selected.name); + } + + @Test + void testIdentityWithRecord() throws Exception { + // Given + ThingRecord t1 = new ThingRecord(1, "a"); + ThingRecord t2 = new ThingRecord(2, "b"); + ExampleRecord input = new ExampleRecord(List.of(t1, t2), t2); + + // When + String json = MAPPER.writeValueAsString(input); + ExampleRecord result = MAPPER.readValue(json, ExampleRecord.class); + + // Then + assertEquals(input.allThings.size(), result.allThings.size()); + assertEquals(input.selected.id, result.selected.id); + assertEquals(input.selected.name, result.selected.name); + } + } \ No newline at end of file From 6ded5d47a7ac8b5abf1eb0e0bd76bfefdc2e788a Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 2 Aug 2025 23:18:27 +0900 Subject: [PATCH 4/6] Use ClassUtil record check --- .../jackson/databind/deser/impl/PropertyValueBuffer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 019d7a7feb..5492ee8f1e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.deser.SettableAnyProperty; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.util.ClassUtil; /** * Simple container used for temporarily buffering a set of @@ -336,7 +337,7 @@ public Object handleIdValue(final DeserializationContext ctxt, Object bean) thro SettableBeanProperty idProp = _objectIdReader.idProperty; if (idProp != null) { // [databind#5328] Records do not have setters, skip...to set id value - if (bean.getClass().isRecord()) { + if (ClassUtil.isRecordType(bean.getClass())) { return bean; } else { return idProp.setAndReturn(bean, _idValue); From e0aca596861a4eb43e498e3fb04ee2e4f03cee9e Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sun, 10 Aug 2025 22:55:06 +0900 Subject: [PATCH 5/6] Modify check to use Creator instead --- .../jackson/databind/deser/impl/PropertyValueBuffer.java | 7 +++---- .../databind/records/JsonIdentityOnRecord5238Test.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 5492ee8f1e..52921f1bd0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -5,8 +5,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.deser.SettableAnyProperty; -import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.databind.deser.*; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.util.ClassUtil; @@ -336,8 +335,8 @@ public Object handleIdValue(final DeserializationContext ctxt, Object bean) thro // also: may need to set a property value as well SettableBeanProperty idProp = _objectIdReader.idProperty; if (idProp != null) { - // [databind#5328] Records do not have setters, skip...to set id value - if (ClassUtil.isRecordType(bean.getClass())) { + // [databind#5328] Records/Creators do not have setters, skip + if (idProp instanceof CreatorProperty) { return bean; } else { return idProp.setAndReturn(bean, _idValue); diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5238Test.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5238Test.java index 2e3512236b..7cc68774a9 100644 --- a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5238Test.java +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/JsonIdentityOnRecord5238Test.java @@ -40,7 +40,7 @@ static class ThingPojo { public final String name; @JsonCreator - public ThingPojo(@JsonProperty("id") int id, @JsonProperty("name") String name) { + public ThingPojo(@JsonProperty("prefixId") int id, @JsonProperty("name") String name) { this.id = id; this.name = name; } From 7d06fc5956b984d5f8aa45b0663662e7c65b8f91 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 18 Aug 2025 10:09:24 -0700 Subject: [PATCH 6/6] Update release notes --- release-notes/VERSION-2.x | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index eb9d560a3d..80e7c939c6 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -10,6 +10,8 @@ Not yet released (reported by @lbonco) #5237: Failing `@JsonMerge` with a custom Map with a `@JsonCreator` constructor (reported by @nlisker) +#5238: Immutable classes with `@JsonIdentityInfo` can be deserialized; records cannot + (fix by Joo-Hyuk K) #5242: Support "binary vectors": `@JsonFormat(shape = Shape.BINARY)` for `float[]`, `double[]` #5257: Deprecate `URL`-taking `readValue()` methods in `ObjectMapper`, `ObjectReader`