diff --git a/src/main/java/org/eclipse/yasson/internal/deserializer/DeserializationModelCreator.java b/src/main/java/org/eclipse/yasson/internal/deserializer/DeserializationModelCreator.java index 1f1bdabd..cebe6c35 100644 --- a/src/main/java/org/eclipse/yasson/internal/deserializer/DeserializationModelCreator.java +++ b/src/main/java/org/eclipse/yasson/internal/deserializer/DeserializationModelCreator.java @@ -180,15 +180,15 @@ private ModelDeserializer deserializerChainInternal(LinkedList return typeDeserializer; } if (Collection.class.isAssignableFrom(rawType)) { - return createCollectionDeserializer(cachedItem, rawType, chain, propertyCustomization); + return createCollectionDeserializer(cachedItem, rawType, chain); } else if (Map.class.isAssignableFrom(rawType)) { - return createMapDeserializer(cachedItem, rawType, chain, propertyCustomization); + return createMapDeserializer(cachedItem, rawType, chain); } else if (rawType.isArray()) { - return createArrayDeserializer(cachedItem, rawType, chain, propertyCustomization); + return createArrayDeserializer(cachedItem, rawType, chain); } else if (type instanceof GenericArrayType) { - return createGenericArray(cachedItem, rawType, chain, propertyCustomization); + return createGenericArray(cachedItem, rawType, chain); } else if (Optional.class.isAssignableFrom(rawType)) { - return createOptionalDeserializer(chain, type, propertyCustomization, cachedItem); + return createOptionalDeserializer(chain, type, cachedItem); } else { return createObjectDeserializer(chain, type, propertyCustomization, classModel, rawType, cachedItem); } @@ -262,8 +262,7 @@ private ModelDeserializer createObjectDeserializer(LinkedList private ModelDeserializer createCollectionDeserializer(CachedItem cachedItem, Class rawType, - LinkedList chain, - Customization propertyCustomization) { + LinkedList chain) { Type type = cachedItem.type; Type colType = type instanceof ParameterizedType ? ((ParameterizedType) type).getActualTypeArguments()[0] @@ -284,8 +283,7 @@ private ModelDeserializer createCollectionDeserializer(CachedItem ca private ModelDeserializer createMapDeserializer(CachedItem cachedItem, Class rawType, - LinkedList chain, - Customization propertyCustomization) { + LinkedList chain) { Type type = cachedItem.type; Type keyType = type instanceof ParameterizedType ? ((ParameterizedType) type).getActualTypeArguments()[0] @@ -298,9 +296,10 @@ private ModelDeserializer createMapDeserializer(CachedItem cachedIte ClassCustomization.empty(), JustReturn.instance(), MAP_KEY_EVENTS); + ClassModel valueClassModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(valueType)); ModelDeserializer valueProcessor = typeProcessor(chain, valueType, - propertyCustomization, + valueClassModel.getClassCustomization(), JustReturn.instance()); MapDeserializer mapDeserializer = new MapDeserializer(keyProcessor, valueProcessor); @@ -315,14 +314,15 @@ private ModelDeserializer createMapDeserializer(CachedItem cachedIte private ModelDeserializer createArrayDeserializer(CachedItem cachedItem, Class rawType, - LinkedList chain, - Customization propertyCustomization) { + LinkedList chain) { JsonbConfigProperties configProperties = jsonbContext.getConfigProperties(); if (rawType.equals(byte[].class) && !configProperties.getBinaryDataStrategy().equals(BinaryDataStrategy.BYTE)) { String strategy = configProperties.getBinaryDataStrategy(); + // Special case for byte[] with base64 encoding - use String's class customization + ClassModel stringModel = jsonbContext.getMappingContext().getOrCreateClassModel(String.class); ModelDeserializer typeProcessor = typeProcessor(chain, String.class, - propertyCustomization, + stringModel.getClassCustomization(), JustReturn.instance()); ModelDeserializer base64Deserializer = ArrayInstanceCreator.createBase64Deserializer(strategy, typeProcessor); @@ -331,22 +331,23 @@ private ModelDeserializer createArrayDeserializer(CachedItem cachedI return nullChecker; } Class arrayType = rawType.getComponentType(); + ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(arrayType); ModelDeserializer typeProcessor = typeProcessor(chain, arrayType, - propertyCustomization, + classModel.getClassCustomization(), JustReturn.instance()); return createArrayCommonDeserializer(cachedItem, rawType, arrayType, typeProcessor); } private ModelDeserializer createGenericArray(CachedItem cachedItem, Class rawType, - LinkedList chain, - Customization propertyCustomization) { + LinkedList chain) { GenericArrayType type = (GenericArrayType) cachedItem.type; Class component = ReflectionUtils.getRawType(type.getGenericComponentType()); + ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(component); ModelDeserializer typeProcessor = typeProcessor(chain, type.getGenericComponentType(), - propertyCustomization, + classModel.getClassCustomization(), JustReturn.instance()); return createArrayCommonDeserializer(cachedItem, rawType, component, typeProcessor); } @@ -365,12 +366,13 @@ private ModelDeserializer createArrayCommonDeserializer(CachedItem c private OptionalDeserializer createOptionalDeserializer(LinkedList chain, Type type, - Customization propertyCustomization, CachedItem cachedItem) { Type colType = type instanceof ParameterizedType ? ((ParameterizedType) type).getActualTypeArguments()[0] : Object.class; - ModelDeserializer typeProcessor = typeProcessor(chain, colType, propertyCustomization, JustReturn.instance()); + colType = ReflectionUtils.resolveType(chain, colType); + ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(colType)); + ModelDeserializer typeProcessor = typeProcessor(chain, colType, classModel.getClassCustomization(), JustReturn.instance()); OptionalDeserializer optionalDeserializer = new OptionalDeserializer(typeProcessor, JustReturn.instance()); models.put(cachedItem, optionalDeserializer); return optionalDeserializer; diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java b/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java index 0379c555..9aa29a7b 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java @@ -302,7 +302,9 @@ private ModelSerializer createMapSerializer(LinkedList chain, Type type, C Type resolvedKey = ReflectionUtils.resolveType(chain, keyType); Class rawClass = ReflectionUtils.getRawType(resolvedKey); ModelSerializer keySerializer = memberSerializer(chain, keyType, ClassCustomization.empty(), true); - ModelSerializer valueSerializer = memberSerializer(chain, valueType, propertyCustomization, false); + Type resolvedValue = ReflectionUtils.resolveType(chain, valueType); + ClassModel valueClassModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(resolvedValue)); + ModelSerializer valueSerializer = memberSerializer(chain, valueType, valueClassModel.getClassCustomization(), false); MapSerializer mapSerializer = MapSerializer.create(rawClass, keySerializer, valueSerializer, jsonbContext); KeyWriter keyWriter = new KeyWriter(mapSerializer); NullVisibilitySwitcher nullVisibilitySwitcher = new NullVisibilitySwitcher(true, keyWriter); @@ -313,7 +315,8 @@ private ModelSerializer createArraySerializer(LinkedList chain, Class raw, Customization propertyCustomization) { Class arrayComponent = raw.getComponentType(); - ModelSerializer modelSerializer = memberSerializer(chain, arrayComponent, propertyCustomization, false); + ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(arrayComponent); + ModelSerializer modelSerializer = memberSerializer(chain, arrayComponent, classModel.getClassCustomization(), false); ModelSerializer arraySerializer = ArraySerializer.create(raw, jsonbContext, modelSerializer); KeyWriter keyWriter = new KeyWriter(arraySerializer); NullVisibilitySwitcher nullVisibilitySwitcher = new NullVisibilitySwitcher(true, keyWriter); @@ -325,7 +328,8 @@ private ModelSerializer createGenericArraySerializer(LinkedList chain, Customization propertyCustomization) { Class raw = ReflectionUtils.getRawType(type); Class component = ReflectionUtils.getRawType(((GenericArrayType) type).getGenericComponentType()); - ModelSerializer modelSerializer = memberSerializer(chain, component, propertyCustomization, false); + ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(component); + ModelSerializer modelSerializer = memberSerializer(chain, component, classModel.getClassCustomization(), false); ModelSerializer arraySerializer = ArraySerializer.create(raw, jsonbContext, modelSerializer); KeyWriter keyWriter = new KeyWriter(arraySerializer); NullVisibilitySwitcher nullVisibilitySwitcher = new NullVisibilitySwitcher(true, keyWriter); @@ -339,7 +343,9 @@ private ModelSerializer createOptionalSerializer(LinkedList chain, Type optType = type instanceof ParameterizedType ? ((ParameterizedType) type).getActualTypeArguments()[0] : Object.class; - ModelSerializer modelSerializer = memberSerializer(chain, optType, propertyCustomization, isKey); + Type resolvedOptType = ReflectionUtils.resolveType(chain, optType); + ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(resolvedOptType)); + ModelSerializer modelSerializer = memberSerializer(chain, optType, classModel.getClassCustomization(), isKey); return new OptionalSerializer(modelSerializer); } diff --git a/src/test/java/org/eclipse/yasson/serializers/TypeDeserializerOnContainersTest.java b/src/test/java/org/eclipse/yasson/serializers/TypeDeserializerOnContainersTest.java new file mode 100644 index 00000000..4b4d8713 --- /dev/null +++ b/src/test/java/org/eclipse/yasson/serializers/TypeDeserializerOnContainersTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2025 IBM and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +package org.eclipse.yasson.serializers; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbConfig; +import jakarta.json.bind.annotation.JsonbTypeDeserializer; +import jakarta.json.bind.config.BinaryDataStrategy; +import jakarta.json.bind.serializer.DeserializationContext; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.stream.JsonParser; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests that {@link jakarta.json.bind.annotation.JsonbTypeDeserializer @JsonbTypeDeserializer} annotated types are + * properly detected and used when those types are used as elements/values in containers (Maps, Collections, + * Arrays, Optionals). + * + * @author James R. Perkins + */ +public class TypeDeserializerOnContainersTest { + + // Test interface with type-level deserializer annotation + @JsonbTypeDeserializer(TestInterfaceDeserializer.class) + public interface TestInterface { + String getValue(); + } + + // Implementation of the test interface + public static class TestImpl implements TestInterface { + private final String value; + + public TestImpl(final String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + } + + // Custom deserializer for TestInterface + public static class TestInterfaceDeserializer implements JsonbDeserializer { + @Override + public TestInterface deserialize(final JsonParser parser, final DeserializationContext ctx, final Type rtType) { + // Parse the JSON object to get the value field + Assertions.assertTrue(parser.hasNext(), "Expected the key name"); + parser.next(); + Assertions.assertTrue(parser.hasNext(), "Expected the value"); + parser.next(); + final String value = parser.getString(); + Assertions.assertTrue(parser.hasNext(), "Expected the end of an object"); + parser.next(); + return new TestImpl("DESERIALIZED:" + value); + } + } + + // Container classes for testing + public static class MapContainer { + public Map map; + } + + public static class ListContainer { + public List list; + } + + public static class ArrayContainer { + public TestInterface[] array; + } + + public static class OptionalContainer { + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public Optional optional; + } + + public static class ByteArrayContainer { + public byte[] data; + } + + private Jsonb jsonb; + + @BeforeEach + public void createJsonb() { + // Create a new Jsonb for each test to avoid type caching + jsonb = JsonbBuilder.create(); + } + + @AfterEach + public void closeJsonb() throws Exception { + if (jsonb != null) { + jsonb.close(); + } + } + + @Test + public void testTypeDeserializerOnMapValues() { + final String json = "{\"map\":{\"key1\":{\"value\":\"value1\"},\"key2\":{\"value\":\"value2\"}}}"; + + final MapContainer result = jsonb.fromJson(json, MapContainer.class); + + Assertions.assertNotNull(result.map); + Assertions.assertEquals(2, result.map.size(), () -> String.format("Expected two entries got %s", result.map)); + Assertions.assertEquals("DESERIALIZED:value1", result.map.get("key1").getValue()); + Assertions.assertEquals("DESERIALIZED:value2", result.map.get("key2").getValue()); + } + + @Test + public void testTypeDeserializerOnListElements() { + final String json = "{\"list\":[{\"value\":\"value1\"},{\"value\":\"value2\"}]}"; + + final ListContainer result = jsonb.fromJson(json, ListContainer.class); + + Assertions.assertNotNull(result.list); + Assertions.assertEquals(2, result.list.size(), () -> String.format("Expected two entries got %s", result.list)); + Assertions.assertEquals("DESERIALIZED:value1", result.list.get(0).getValue()); + Assertions.assertEquals("DESERIALIZED:value2", result.list.get(1).getValue()); + } + + @Test + public void testTypeDeserializerOnArrayElements() { + final String json = "{\"array\":[{\"value\":\"value1\"},{\"value\":\"value2\"}]}"; + + final ArrayContainer result = jsonb.fromJson(json, ArrayContainer.class); + + Assertions.assertNotNull(result.array); + Assertions.assertEquals(2, result.array.length, () -> String.format("Expected two entries got %s", Arrays.toString(result.array))); + Assertions.assertEquals("DESERIALIZED:value1", result.array[0].getValue()); + Assertions.assertEquals("DESERIALIZED:value2", result.array[1].getValue()); + } + + @Test + public void testTypeDeserializerOnOptionalValue() { + final String json = "{\"optional\":{\"value\":\"value1\"}}"; + + final OptionalContainer result = jsonb.fromJson(json, OptionalContainer.class); + + Assertions.assertNotNull(result.optional); + Assertions.assertTrue(result.optional.isPresent(), "Expected value to be present, but the optional was empty."); + Assertions.assertEquals("DESERIALIZED:value1", result.optional.get().getValue()); + } + + @Test + public void testTypeDeserializerOnByteArray() { + final String json = "{\"data\":[1,2,3,4,5]}"; + + final ByteArrayContainer result = jsonb.fromJson(json, ByteArrayContainer.class); + + Assertions.assertNotNull(result.data); + Assertions.assertEquals(5, result.data.length); + Assertions.assertArrayEquals(new byte[]{1, 2, 3, 4, 5}, result.data); + } + + @Test + public void testTypeDeserializerOnByteArrayWithBase64() throws Exception { + try (Jsonb base64Jsonb = JsonbBuilder.create(new JsonbConfig() + .withBinaryDataStrategy(BinaryDataStrategy.BASE_64))) { + + // "SGVsbG8=" is "Hello" in base64 + final String json = "{\"data\":\"SGVsbG8=\"}"; + + final ByteArrayContainer result = base64Jsonb.fromJson(json, ByteArrayContainer.class); + + Assertions.assertNotNull(result.data); + Assertions.assertArrayEquals("Hello".getBytes(), result.data); + } + } +} diff --git a/src/test/java/org/eclipse/yasson/serializers/TypeSerializerOnContainersTest.java b/src/test/java/org/eclipse/yasson/serializers/TypeSerializerOnContainersTest.java new file mode 100644 index 00000000..a3edc458 --- /dev/null +++ b/src/test/java/org/eclipse/yasson/serializers/TypeSerializerOnContainersTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2025 IBM and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +package org.eclipse.yasson.serializers; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.annotation.JsonbTypeSerializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import jakarta.json.bind.serializer.SerializationContext; +import jakarta.json.stream.JsonGenerator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests that {@link jakarta.json.bind.annotation.JsonbTypeSerializer @JsonbTypeSerializer} annotated types are + * properly detected and used when those types are used as elements/values in containers (Maps, Collections, + * Arrays, Optionals). + * + * @author James R. Perkins + */ +public class TypeSerializerOnContainersTest { + + // Test interface with type-level serializer annotation + @JsonbTypeSerializer(TestInterfaceSerializer.class) + public interface TestInterface { + String getValue(); + } + + // Implementation of the test interface + public static class TestImpl implements TestInterface { + private final String value; + + public TestImpl(final String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + } + + // Custom serializer for TestInterface + public static class TestInterfaceSerializer implements JsonbSerializer { + @Override + public void serialize(final TestInterface obj, final JsonGenerator generator, final SerializationContext ctx) { + generator.write("SERIALIZED:" + obj.getValue()); + } + } + + // Container classes for testing + public static class MapContainer { + public Map map; + + public MapContainer(Map map) { + this.map = map; + } + } + + public static class ListContainer { + public List list; + + public ListContainer(List list) { + this.list = list; + } + } + + public static class ArrayContainer { + public TestInterface[] array; + + public ArrayContainer(TestInterface[] array) { + this.array = array; + } + } + + public static class OptionalContainer { + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public Optional optional; + + public OptionalContainer(Optional optional) { + this.optional = optional; + } + } + + private Jsonb jsonb; + + @BeforeEach + public void createJsonb() { + // Create a new Jsonb for each test to avoid type caching + jsonb = JsonbBuilder.create(); + } + + @AfterEach + public void closeJsonb() throws Exception { + if (jsonb != null) { + jsonb.close(); + } + } + + @Test + public void testTypeSerializerOnMapValues() { + final MapContainer container = new MapContainer(Map.of( + "key1", new TestImpl("value1"), + "key2", new TestImpl("value2") + )); + + final String json = jsonb.toJson(container); + + Assertions.assertTrue(json.contains("\"key1\":\"SERIALIZED:value1\""), + "Expected serialized value1 but got: " + json); + Assertions.assertTrue(json.contains("\"key2\":\"SERIALIZED:value2\""), + "Expected serialized value2 but got: " + json); + } + + @Test + public void testTypeSerializerOnListElements() { + final ListContainer container = new ListContainer(List.of( + new TestImpl("value1"), + new TestImpl("value2") + )); + + final String json = jsonb.toJson(container); + + Assertions.assertEquals("{\"list\":[\"SERIALIZED:value1\",\"SERIALIZED:value2\"]}", json); + } + + @Test + public void testTypeSerializerOnArrayElements() { + final ArrayContainer container = new ArrayContainer(new TestInterface[]{ + new TestImpl("value1"), + new TestImpl("value2") + }); + + final String json = jsonb.toJson(container); + + Assertions.assertEquals("{\"array\":[\"SERIALIZED:value1\",\"SERIALIZED:value2\"]}", json); + } + + @Test + public void testTypeSerializerOnOptionalValue() { + final OptionalContainer container = new OptionalContainer(Optional.of(new TestImpl("value1"))); + + final String json = jsonb.toJson(container); + + Assertions.assertEquals("{\"optional\":\"SERIALIZED:value1\"}", json); + } +}