From 0ea39cc87800c2706d6c76014e7b81e13cead6ed Mon Sep 17 00:00:00 2001 From: Raushan Date: Sat, 27 Sep 2025 00:10:01 +0530 Subject: [PATCH 1/2] [652-603] Add tests for @JsonbTypeAdapter consistency with collections and maps --- .../JsonbTypeAdapterCollectionTest.java | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 src/test/java/org/eclipse/yasson/adapters/JsonbTypeAdapterCollectionTest.java diff --git a/src/test/java/org/eclipse/yasson/adapters/JsonbTypeAdapterCollectionTest.java b/src/test/java/org/eclipse/yasson/adapters/JsonbTypeAdapterCollectionTest.java new file mode 100644 index 000000000..a4686b366 --- /dev/null +++ b/src/test/java/org/eclipse/yasson/adapters/JsonbTypeAdapterCollectionTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2016, 2025 Oracle 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.adapters; + +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbConfig; +import jakarta.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for @JsonbTypeAdapter consistency issues with collections and maps. + * These tests verify the fix for the bug where class-level @JsonbTypeAdapter + * worked for single objects but was ignored for collection elements. + * Issue#652 & + * Issue#603 + */ +public class JsonbTypeAdapterCollectionTest { + + @Test + public void classLevelAdapterShouldApplyToSingleObjects() throws Exception { + ClassLevelAdapterType obj = new ClassLevelAdapterType(); + + try (Jsonb jsonb = JsonbBuilder.create()) { + String json = jsonb.toJson(obj); + assertEquals("\"adapted_value\"", json); + } + } + + @Test + public void classLevelAdapterShouldApplyToCollectionElements() throws Exception { + ClassLevelAdapterType obj = new ClassLevelAdapterType(); + + try (Jsonb jsonb = JsonbBuilder.create()) { + String json = jsonb.toJson(Set.of(obj)); + assertEquals("[\"adapted_value\"]", json); + } + } + + @Test + public void classLevelAdapterShouldApplyToListElements() throws Exception { + ClassLevelAdapterType obj1 = new ClassLevelAdapterType(); + ClassLevelAdapterType obj2 = new ClassLevelAdapterType(); + + try (Jsonb jsonb = JsonbBuilder.create()) { + String json = jsonb.toJson(List.of(obj1, obj2)); + assertEquals("[\"adapted_value\",\"adapted_value\"]", json); + } + } + + @Test + public void classLevelAdapterShouldApplyToArrayElements() throws Exception { + ClassLevelAdapterType obj1 = new ClassLevelAdapterType(); + ClassLevelAdapterType obj2 = new ClassLevelAdapterType(); + ClassLevelAdapterType[] array = {obj1, obj2}; + + try (Jsonb jsonb = JsonbBuilder.create()) { + String json = jsonb.toJson(array); + assertEquals("[\"adapted_value\",\"adapted_value\"]", json); + } + } + + @Test + public void classLevelAdapterShouldApplyConsistentlyRegardlessOfOrder() throws Exception { + ClassLevelAdapterType obj = new ClassLevelAdapterType(); + + // Test collection first, then single - should be consistent + try (var jsonb = JsonbBuilder.newBuilder().build()) { + String collectionJson = jsonb.toJson(Set.of(obj)); + String singleJson = jsonb.toJson(obj); + + assertEquals("[\"adapted_value\"]", collectionJson); + assertEquals("\"adapted_value\"", singleJson); + } + + // Test single first, then collection - should be consistent + try (var jsonb = JsonbBuilder.newBuilder().build()) { + String singleJson = jsonb.toJson(obj); + String collectionJson = jsonb.toJson(Set.of(obj)); + + assertEquals("\"adapted_value\"", singleJson); + assertEquals("[\"adapted_value\"]", collectionJson); + } + } + + @Test + public void configAdapterWorksCorrectlyForCollectionElements() throws Exception { + ClassLevelAdapterType obj = new ClassLevelAdapterType(); + var config = new JsonbConfig().withAdapters(new ClassLevelAdapter()); + + try (var jsonb = JsonbBuilder.newBuilder().withConfig(config).build()) { + String single = jsonb.toJson(obj); + String set = jsonb.toJson(Set.of(obj)); + + assertEquals("\"adapted_value\"", single); + assertEquals("[\"adapted_value\"]", set); + } + } + + @Test + public void fieldLevelAdapterShouldApplyToFieldsInObjects() throws Exception { + ContainerWithFieldAdapter container = new ContainerWithFieldAdapter(); + + try (Jsonb jsonb = JsonbBuilder.create()) { + String json = jsonb.toJson(container); + assertEquals("{\"item\":\"field_adapted\"}", json); + } + } + + @Test + public void fieldLevelAdapterShouldApplyToFieldsInCollections() throws Exception { + ContainerWithFieldAdapter container1 = new ContainerWithFieldAdapter(); + ContainerWithFieldAdapter container2 = new ContainerWithFieldAdapter(); + + try (Jsonb jsonb = JsonbBuilder.create()) { + String listJson = jsonb.toJson(List.of(container1, container2)); + assertEquals("[{\"item\":\"field_adapted\"},{\"item\":\"field_adapted\"}]", listJson); + } + } + + @Test + public void fieldLevelAdapterShouldApplyToFieldsInArrays() throws Exception { + ContainerWithFieldAdapter container1 = new ContainerWithFieldAdapter(); + ContainerWithFieldAdapter container2 = new ContainerWithFieldAdapter(); + ContainerWithFieldAdapter[] array = {container1, container2}; + + try (Jsonb jsonb = JsonbBuilder.create()) { + String arrayJson = jsonb.toJson(array); + assertEquals("[{\"item\":\"field_adapted\"},{\"item\":\"field_adapted\"}]", arrayJson); + } + } + + @Test + public void nestedCollectionsWithAdaptersShouldWork() throws Exception { + ClassLevelAdapterType obj1 = new ClassLevelAdapterType(); + ClassLevelAdapterType obj2 = new ClassLevelAdapterType(); + List> nested = List.of( + List.of(obj1, obj2), + List.of(obj1) + ); + + try (Jsonb jsonb = JsonbBuilder.create()) { + String json = jsonb.toJson(nested); + assertEquals("[[\"adapted_value\",\"adapted_value\"],[\"adapted_value\"]]", json); + } + } + + @Test + public void mixedCollectionTypesWithAdaptersShouldWork() throws Exception { + ClassLevelAdapterType obj1 = new ClassLevelAdapterType(); + ClassLevelAdapterType obj2 = new ClassLevelAdapterType(); + + try (Jsonb jsonb = JsonbBuilder.create()) { + // Test List + String listJson = jsonb.toJson(List.of(obj1, obj2)); + assertEquals("[\"adapted_value\",\"adapted_value\"]", listJson); + + // Test Set (reusing the same jsonb instance to test caching) + String setJson = jsonb.toJson(Set.of(obj1, obj2)); + assertEquals("[\"adapted_value\",\"adapted_value\"]", setJson); + + // Test Array (reusing the same jsonb instance to test caching) + ClassLevelAdapterType[] array = {obj1, obj2}; + String arrayJson = jsonb.toJson(array); + assertEquals("[\"adapted_value\",\"adapted_value\"]", arrayJson); + } + } + + @Test + public void adapterShouldApplyToMapValues() throws Exception { + String key1 = "key1"; + String key2 = "key2"; + ClassLevelAdapterType value1 = new ClassLevelAdapterType(); + ClassLevelAdapterType value2 = new ClassLevelAdapterType(); + Map map = Map.of(key1, value1, key2, value2); + + try (var jsonb = JsonbBuilder.newBuilder().build()) { + String json = jsonb.toJson(map); + // Values should use class-level adapter + // Map with string keys serializes as a plain JSON object + boolean isCorrect = json.equals("{\"key1\":\"adapted_value\",\"key2\":\"adapted_value\"}") || + json.equals("{\"key2\":\"adapted_value\",\"key1\":\"adapted_value\"}"); + assertTrue(isCorrect, "Expected map values to use adapter, got: " + json); + } + } + + @Test + public void adapterShouldApplyToBothMapKeysAndValues() throws Exception { + ClassLevelAdapterType key = new ClassLevelAdapterType(); + ClassLevelAdapterType value = new ClassLevelAdapterType(); + Map map = Map.of(key, value); + + try (var jsonb = JsonbBuilder.newBuilder().build()) { + String json = jsonb.toJson(map); + // Both key and value should use class-level adapter + assertEquals("[{\"key\":\"adapted_value\",\"value\":\"adapted_value\"}]", json); + } + } + + @Test + public void adapterShouldApplyToMapKeysWithMixedValueTypes() throws Exception { + ClassLevelAdapterType key = new ClassLevelAdapterType(); + ContainerWithFieldAdapter value = new ContainerWithFieldAdapter(); + Map map = Map.of(key, value); + + try (var jsonb = JsonbBuilder.newBuilder().build()) { + String json = jsonb.toJson(map); + // Key should use class-level adapter, value should use field-level adapter for its field + assertEquals("[{\"key\":\"adapted_value\",\"value\":{\"item\":\"field_adapted\"}}]", json); + } + } + + @Test + public void mapKeysWithoutAdapterShouldSerializeNormally() throws Exception { + SimpleType key = new SimpleType(); + SimpleType value = new SimpleType(); + Map map = Map.of(key, value); + + try (var jsonb = JsonbBuilder.newBuilder().build()) { + String json = jsonb.toJson(map); + assertEquals("[{\"key\":{\"data\":\"simple\"},\"value\":{\"data\":\"simple\"}}]", json); + } + } + + // Helper classes for testing + @JsonbTypeAdapter(ClassLevelAdapter.class) + public static class ClassLevelAdapterType { + public String wrong = "should_not_appear"; + } + + public static class ClassLevelAdapter implements JsonbAdapter { + @Override + public String adaptToJson(ClassLevelAdapterType obj) throws Exception { + return "adapted_value"; + } + + @Override + public ClassLevelAdapterType adaptFromJson(String obj) throws Exception { + throw new UnsupportedOperationException("Deserialization not implemented for testing purposes"); + } + } + + public static class ContainerWithFieldAdapter { + @JsonbTypeAdapter(FieldLevelAdapter.class) + public final SimpleType item = new SimpleType(); + } + + public static class FieldLevelAdapter implements JsonbAdapter { + @Override + public String adaptToJson(SimpleType obj) throws Exception { + return "field_adapted"; + } + + @Override + public SimpleType adaptFromJson(String obj) throws Exception { + throw new UnsupportedOperationException("not implemented"); + } + } + + public static class SimpleType { + public String data = "simple"; + + @Override + public String toString() { + return "SimpleType"; + } + } +} From e8c90f68321a75bc5fe2e830fcf868bea348efc8 Mon Sep 17 00:00:00 2001 From: Raushan Date: Sat, 27 Sep 2025 00:11:22 +0530 Subject: [PATCH 2/2] [652]: Enhance serialization to respect class-level @JsonbTypeAdapter annotations Signed-off-by: Raushan --- .../serializer/SerializationModelCreator.java | 40 +++++++++++++------ .../types/ObjectTypeSerializer.java | 22 +++++++++- 2 files changed, 48 insertions(+), 14 deletions(-) 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 0379c5550..3a0ae0b5c 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025 Oracle 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 @@ -22,6 +22,7 @@ import java.util.LinkedList; import java.util.ListIterator; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -142,17 +143,10 @@ private ModelSerializer serializerChainInternal(LinkedList chain, boolean rootValue, boolean isKey, boolean resolveRootAdapter) { - if (explicitChain.containsKey(type)) { - return explicitChain.get(type); - } - Class rawType = ReflectionUtils.getRawType(type); - Optional serializerBinding = userSerializer(type, - (ComponentBoundCustomization) propertyCustomization); - if (serializerBinding.isPresent()) { - return serializerBinding.get(); - } - if (resolveRootAdapter) { - Optional maybeAdapter = adapterBinding(type, (ComponentBoundCustomization) propertyCustomization); + // Check for adapter binding first, even if we have a cached serializer + if (resolveRootAdapter && propertyCustomization instanceof ComponentBoundCustomization) { + ComponentBoundCustomization componentBound = (ComponentBoundCustomization) propertyCustomization; + Optional maybeAdapter = adapterBinding(type, componentBound); if (maybeAdapter.isPresent()) { AdapterBinding adapterBinding = maybeAdapter.get(); Type toType = adapterBinding.getToType(); @@ -170,6 +164,17 @@ private ModelSerializer serializerChainInternal(LinkedList chain, } } + if (explicitChain.containsKey(type)) { + return explicitChain.get(type); + } + Class rawType = ReflectionUtils.getRawType(type); + Optional serializerBinding = propertyCustomization instanceof ComponentBoundCustomization + ? userSerializer(type, (ComponentBoundCustomization) propertyCustomization) + : Optional.empty(); + if (serializerBinding.isPresent()) { + return serializerBinding.get(); + } + ModelSerializer typeSerializer = null; if (!Object.class.equals(rawType)) { typeSerializer = TypeSerializers.getTypeSerializer(chain, rawType, propertyCustomization, jsonbContext, isKey); @@ -356,6 +361,17 @@ private ModelSerializer memberSerializer(LinkedList chain, return serializerBinding.get(); } Optional maybeAdapter = adapterBinding(resolved, (ComponentBoundCustomization) customization); + // If no adapter found, also check the type's own class customization as a fallback + // This ensures class-level @JsonbTypeAdapter annotations are always considered + if (maybeAdapter.isEmpty()) { + ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(rawType); + ComponentBoundCustomization classCustomization = classModel.getClassCustomization(); + // Only check class customization if it's different from what we already checked + if (customization == null || !Objects.equals(classCustomization, customization)) { + maybeAdapter = adapterBinding(resolved, classCustomization); + } + } + if (maybeAdapter.isPresent()) { AdapterBinding adapterBinding = maybeAdapter.get(); Type toType = adapterBinding.getToType(); diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/types/ObjectTypeSerializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/types/ObjectTypeSerializer.java index db9ef9539..0d25cceb9 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/types/ObjectTypeSerializer.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/types/ObjectTypeSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025 Oracle 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 @@ -21,6 +21,8 @@ import jakarta.json.stream.JsonGenerator; import org.eclipse.yasson.internal.SerializationContextImpl; +import org.eclipse.yasson.internal.model.ClassModel; +import org.eclipse.yasson.internal.model.customization.ComponentBoundCustomization; import org.eclipse.yasson.internal.model.customization.Customization; import org.eclipse.yasson.internal.serializer.ModelSerializer; import org.eclipse.yasson.internal.serializer.SerializationModelCreator; @@ -64,10 +66,26 @@ private void findSerializer(Object key, JsonGenerator generator, SerializationCo Class clazz = key.getClass(); cache.computeIfAbsent(clazz, aClass -> { SerializationModelCreator serializationModelCreator = context.getJsonbContext().getSerializationModelCreator(); - return serializationModelCreator.serializerChainRuntime(new LinkedList<>(chain), clazz, customization, false, isKey); + // For regular objects, check if the class has its own customization with potential adapter bindings + // This ensures that class-level annotations like @JsonbTypeAdapter are respected + ClassModel classModel = context.getJsonbContext().getMappingContext().getOrCreateClassModel(aClass); + Customization effectiveCustomization = getEffectiveCustomization(classModel); + return serializationModelCreator.serializerChainRuntime(new LinkedList<>(chain), aClass, effectiveCustomization, false, isKey); }).serialize(key, generator, context); } + private Customization getEffectiveCustomization(ClassModel classModel) { + Customization classCustomization = classModel.getClassCustomization(); + + // Use class customization if it has adapter bindings, otherwise use the context customization + boolean hasAdapterBinding = false; + if (classCustomization instanceof ComponentBoundCustomization) { + ComponentBoundCustomization componentBound = (ComponentBoundCustomization) classCustomization; + hasAdapterBinding = componentBound.getSerializeAdapterBinding() != null; + } + return hasAdapterBinding ? classCustomization : customization; + } + /** * Add serializer to the cache. *