From 0804c146b756e5a8973f4e18c6a147f666030c9b Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Wed, 15 Oct 2025 16:52:57 -0700 Subject: [PATCH 1/2] [685] Ensure the value type of a container being deserialized uses the value types class model for custom deserialization. Do the same for serialization. Signed-off-by: James R. Perkins --- .../DeserializationModelCreator.java | 40 ++-- .../serializer/SerializationModelCreator.java | 14 +- .../TypeDeserializerOnContainersTest.java | 188 ++++++++++++++++++ .../TypeSerializerOnContainersTest.java | 163 +++++++++++++++ 4 files changed, 382 insertions(+), 23 deletions(-) create mode 100644 src/test/java/org/eclipse/yasson/serializers/TypeDeserializerOnContainersTest.java create mode 100644 src/test/java/org/eclipse/yasson/serializers/TypeSerializerOnContainersTest.java 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); + } +} From 2439bca6a9eeb67cce4205f6f95569f7960ad854 Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Fri, 21 Nov 2025 10:21:45 -0800 Subject: [PATCH 2/2] [685] Fix the issue when deserializing a Map which has a wildcard value. Added tests for wildcards on map keys and values, lists and optional. Signed-off-by: James R. Perkins --- .../DeserializationModelCreator.java | 2 +- .../TypeDeserializerOnContainersTest.java | 31 ++++++- .../TypeSerializerOnContainersTest.java | 92 ++++++++++++++----- 3 files changed, 99 insertions(+), 26 deletions(-) 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 cebe6c35..6a6dacab 100644 --- a/src/main/java/org/eclipse/yasson/internal/deserializer/DeserializationModelCreator.java +++ b/src/main/java/org/eclipse/yasson/internal/deserializer/DeserializationModelCreator.java @@ -296,7 +296,7 @@ private ModelDeserializer createMapDeserializer(CachedItem cachedIte ClassCustomization.empty(), JustReturn.instance(), MAP_KEY_EVENTS); - ClassModel valueClassModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(valueType)); + ClassModel valueClassModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.resolveRawType(chain, valueType)); ModelDeserializer valueProcessor = typeProcessor(chain, valueType, valueClassModel.getClassCustomization(), diff --git a/src/test/java/org/eclipse/yasson/serializers/TypeDeserializerOnContainersTest.java b/src/test/java/org/eclipse/yasson/serializers/TypeDeserializerOnContainersTest.java index 4b4d8713..7cd1e3f2 100644 --- a/src/test/java/org/eclipse/yasson/serializers/TypeDeserializerOnContainersTest.java +++ b/src/test/java/org/eclipse/yasson/serializers/TypeDeserializerOnContainersTest.java @@ -79,19 +79,23 @@ public TestInterface deserialize(final JsonParser parser, final DeserializationC // Container classes for testing public static class MapContainer { public Map map; + public Map questionKeyMap; + public Map questionValueMap; } public static class ListContainer { public List list; + public List questionList; } public static class ArrayContainer { public TestInterface[] array; } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public static class OptionalContainer { - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public Optional optional; + public Optional questionOptional; } public static class ByteArrayContainer { @@ -115,7 +119,7 @@ public void closeJsonb() throws Exception { @Test public void testTypeDeserializerOnMapValues() { - final String json = "{\"map\":{\"key1\":{\"value\":\"value1\"},\"key2\":{\"value\":\"value2\"}}}"; + final String json = "{\"map\":{\"key1\":{\"value\":\"value1\"},\"key2\":{\"value\":\"value2\"}}, \"questionKeyMap\":{\"qKey1\":\"value1\",\"qKey2\":\"value2\"},\"questionValueMap\":{\"key1\":\"qValue1\",\"key2\":\"qValue2\"}}"; final MapContainer result = jsonb.fromJson(json, MapContainer.class); @@ -123,11 +127,21 @@ public void testTypeDeserializerOnMapValues() { 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()); + + Assertions.assertNotNull(result.questionKeyMap); + Assertions.assertEquals(2, result.questionKeyMap.size(), () -> String.format("Expected two entries got %s", result.questionKeyMap)); + Assertions.assertEquals("value1", result.questionKeyMap.get("qKey1")); + Assertions.assertEquals("value2", result.questionKeyMap.get("qKey2")); + + Assertions.assertNotNull(result.questionValueMap); + Assertions.assertEquals(2, result.questionValueMap.size(), () -> String.format("Expected two entries got %s", result.questionValueMap)); + Assertions.assertEquals("qValue1", result.questionValueMap.get("key1")); + Assertions.assertEquals("qValue2", result.questionValueMap.get("key2")); } @Test public void testTypeDeserializerOnListElements() { - final String json = "{\"list\":[{\"value\":\"value1\"},{\"value\":\"value2\"}]}"; + final String json = "{\"list\":[{\"value\":\"value1\"},{\"value\":\"value2\"}], \"questionList\": [\"value1\", \"value2\"]}"; final ListContainer result = jsonb.fromJson(json, ListContainer.class); @@ -135,6 +149,11 @@ public void testTypeDeserializerOnListElements() { 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()); + + Assertions.assertNotNull(result.questionList); + Assertions.assertEquals(2, result.questionList.size(), () -> String.format("Expected two entries got %s", result.questionList)); + Assertions.assertEquals("value1", result.questionList.get(0)); + Assertions.assertEquals("value2", result.questionList.get(1)); } @Test @@ -151,13 +170,17 @@ public void testTypeDeserializerOnArrayElements() { @Test public void testTypeDeserializerOnOptionalValue() { - final String json = "{\"optional\":{\"value\":\"value1\"}}"; + final String json = "{\"optional\":{\"value\":\"value1\"},\"questionOptional\":\"value2\"}"; 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()); + + Assertions.assertNotNull(result.questionOptional); + Assertions.assertTrue(result.questionOptional.isPresent(), "Expected value to be present, but the optional was empty."); + Assertions.assertEquals("value2", result.questionOptional.get()); } @Test diff --git a/src/test/java/org/eclipse/yasson/serializers/TypeSerializerOnContainersTest.java b/src/test/java/org/eclipse/yasson/serializers/TypeSerializerOnContainersTest.java index a3edc458..89f3ff2d 100644 --- a/src/test/java/org/eclipse/yasson/serializers/TypeSerializerOnContainersTest.java +++ b/src/test/java/org/eclipse/yasson/serializers/TypeSerializerOnContainersTest.java @@ -12,12 +12,19 @@ package org.eclipse.yasson.serializers; +import java.io.StringReader; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbException; import jakarta.json.bind.annotation.JsonbTypeSerializer; import jakarta.json.bind.serializer.JsonbSerializer; import jakarta.json.bind.serializer.SerializationContext; @@ -66,35 +73,43 @@ public void serialize(final TestInterface obj, final JsonGenerator generator, fi // Container classes for testing public static class MapContainer { - public Map map; + public final Map map; + public final Map questionKeyMap; + public final Map questionValueMap; - public MapContainer(Map map) { + public MapContainer(final Map map, final Map questionKeyMap, final Map questionValueMap) { this.map = map; + this.questionKeyMap = questionKeyMap; + this.questionValueMap = questionValueMap; } } public static class ListContainer { - public List list; + public final List list; + public final List questionList; - public ListContainer(List list) { + public ListContainer(final List list, final List questionList) { this.list = list; + this.questionList = questionList; } } public static class ArrayContainer { - public TestInterface[] array; + public final TestInterface[] array; public ArrayContainer(TestInterface[] array) { this.array = array; } } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public static class OptionalContainer { - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - public Optional optional; + public final Optional optional; + public final Optional questionOptional; - public OptionalContainer(Optional optional) { + public OptionalContainer(final Optional optional, final Optional questionOptional) { this.optional = optional; + this.questionOptional = questionOptional; } } @@ -118,14 +133,28 @@ public void testTypeSerializerOnMapValues() { final MapContainer container = new MapContainer(Map.of( "key1", new TestImpl("value1"), "key2", new TestImpl("value2") - )); + ), Map.of("qKey1", "value1", "qKey2", "value2"), + Map.of("key1", "qValue1", "key2", "qValue2") + ); + + final JsonObject json = toJsonObject(container); + final JsonObject map = json.getJsonObject("map"); + final JsonObject questionKeyMap = json.getJsonObject("questionKeyMap"); + final JsonObject questionValueMap = json.getJsonObject("questionValueMap"); + + Supplier errorMessage = () -> String.format("Expected value not found in %s", map); + Assertions.assertEquals("SERIALIZED:value1", map.getString("key1"), errorMessage); + Assertions.assertEquals("SERIALIZED:value2", map.getString("key2"), errorMessage); - 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); + errorMessage = () -> String.format("Expected value not found in %s", questionKeyMap); + Assertions.assertEquals("value1", questionKeyMap.getString("qKey1"), errorMessage); + Assertions.assertEquals("value2", questionKeyMap.getString("qKey2"), errorMessage); + + + errorMessage = () -> String.format("Expected value not found in %s", questionValueMap); + Assertions.assertEquals("qValue1", questionValueMap.getString("key1"), errorMessage); + Assertions.assertEquals("qValue2", questionValueMap.getString("key2"), errorMessage); } @Test @@ -133,16 +162,26 @@ public void testTypeSerializerOnListElements() { final ListContainer container = new ListContainer(List.of( new TestImpl("value1"), new TestImpl("value2") - )); + ), List.of("qValue1", "qValue2")); - final String json = jsonb.toJson(container); + final JsonObject json = toJsonObject(container); + final JsonArray list = json.getJsonArray("list"); + final JsonArray questionList = json.getJsonArray("questionList"); - Assertions.assertEquals("{\"list\":[\"SERIALIZED:value1\",\"SERIALIZED:value2\"]}", json); + Supplier errorMessage = () -> String.format("Expected value not found in %s", list); + Assertions.assertEquals(2, list.size(), () -> String.format("Expected a size of 2 in %s", list)); + Assertions.assertEquals("SERIALIZED:value1", list.getString(0), errorMessage); + Assertions.assertEquals("SERIALIZED:value2", list.getString(1), errorMessage); + + errorMessage = () -> String.format("Expected value not found in %s", questionList); + Assertions.assertEquals(2, questionList.size(), () -> String.format("Expected a size of 2 in %s", questionList)); + Assertions.assertEquals("qValue1", questionList.getString(0), errorMessage); + Assertions.assertEquals("qValue2", questionList.getString(1), errorMessage); } @Test public void testTypeSerializerOnArrayElements() { - final ArrayContainer container = new ArrayContainer(new TestInterface[]{ + final ArrayContainer container = new ArrayContainer(new TestInterface[] { new TestImpl("value1"), new TestImpl("value2") }); @@ -154,10 +193,21 @@ public void testTypeSerializerOnArrayElements() { @Test public void testTypeSerializerOnOptionalValue() { - final OptionalContainer container = new OptionalContainer(Optional.of(new TestImpl("value1"))); + final OptionalContainer container = new OptionalContainer(Optional.of(new TestImpl("value1")), Optional.of("value2")); - final String json = jsonb.toJson(container); + final JsonObject json = toJsonObject(container); - Assertions.assertEquals("{\"optional\":\"SERIALIZED:value1\"}", json); + Assertions.assertEquals("SERIALIZED:value1", json.getString("optional")); + Assertions.assertEquals("value2", json.getString("questionOptional")); + } + + private JsonObject toJsonObject(final Object object) throws JsonbException { + final String value = jsonb.toJson(object); + try ( + StringReader reader = new StringReader(value); + JsonReader jsonReader = Json.createReader(reader) + ) { + return jsonReader.readObject(); + } } }