Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,15 @@ private ModelDeserializer<JsonParser> deserializerChainInternal(LinkedList<Type>
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);
}
Expand Down Expand Up @@ -262,8 +262,7 @@ private ModelDeserializer<JsonParser> createObjectDeserializer(LinkedList<Type>

private ModelDeserializer<JsonParser> createCollectionDeserializer(CachedItem cachedItem,
Class<?> rawType,
LinkedList<Type> chain,
Customization propertyCustomization) {
LinkedList<Type> chain) {
Type type = cachedItem.type;
Type colType = type instanceof ParameterizedType
? ((ParameterizedType) type).getActualTypeArguments()[0]
Expand All @@ -284,8 +283,7 @@ private ModelDeserializer<JsonParser> createCollectionDeserializer(CachedItem ca

private ModelDeserializer<JsonParser> createMapDeserializer(CachedItem cachedItem,
Class<?> rawType,
LinkedList<Type> chain,
Customization propertyCustomization) {
LinkedList<Type> chain) {
Type type = cachedItem.type;
Type keyType = type instanceof ParameterizedType
? ((ParameterizedType) type).getActualTypeArguments()[0]
Expand All @@ -298,9 +296,10 @@ private ModelDeserializer<JsonParser> createMapDeserializer(CachedItem cachedIte
ClassCustomization.empty(),
JustReturn.instance(),
MAP_KEY_EVENTS);
ClassModel valueClassModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(valueType));
ModelDeserializer<JsonParser> valueProcessor = typeProcessor(chain,
valueType,
propertyCustomization,
valueClassModel.getClassCustomization(),
JustReturn.instance());

MapDeserializer mapDeserializer = new MapDeserializer(keyProcessor, valueProcessor);
Expand All @@ -315,14 +314,15 @@ private ModelDeserializer<JsonParser> createMapDeserializer(CachedItem cachedIte

private ModelDeserializer<JsonParser> createArrayDeserializer(CachedItem cachedItem,
Class<?> rawType,
LinkedList<Type> chain,
Customization propertyCustomization) {
LinkedList<Type> 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<JsonParser> typeProcessor = typeProcessor(chain,
String.class,
propertyCustomization,
stringModel.getClassCustomization(),
JustReturn.instance());
ModelDeserializer<JsonParser> base64Deserializer = ArrayInstanceCreator.createBase64Deserializer(strategy,
typeProcessor);
Expand All @@ -331,22 +331,23 @@ private ModelDeserializer<JsonParser> createArrayDeserializer(CachedItem cachedI
return nullChecker;
}
Class<?> arrayType = rawType.getComponentType();
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(arrayType);
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain,
arrayType,
propertyCustomization,
classModel.getClassCustomization(),
JustReturn.instance());
return createArrayCommonDeserializer(cachedItem, rawType, arrayType, typeProcessor);
}

private ModelDeserializer<JsonParser> createGenericArray(CachedItem cachedItem,
Class<?> rawType,
LinkedList<Type> chain,
Customization propertyCustomization) {
LinkedList<Type> chain) {
GenericArrayType type = (GenericArrayType) cachedItem.type;
Class<?> component = ReflectionUtils.getRawType(type.getGenericComponentType());
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(component);
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain,
type.getGenericComponentType(),
propertyCustomization,
classModel.getClassCustomization(),
JustReturn.instance());
return createArrayCommonDeserializer(cachedItem, rawType, component, typeProcessor);
}
Expand All @@ -365,12 +366,13 @@ private ModelDeserializer<JsonParser> createArrayCommonDeserializer(CachedItem c

private OptionalDeserializer createOptionalDeserializer(LinkedList<Type> chain,
Type type,
Customization propertyCustomization,
CachedItem cachedItem) {
Type colType = type instanceof ParameterizedType
? ((ParameterizedType) type).getActualTypeArguments()[0]
: Object.class;
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain, colType, propertyCustomization, JustReturn.instance());
colType = ReflectionUtils.resolveType(chain, colType);
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(colType));
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain, colType, classModel.getClassCustomization(), JustReturn.instance());
OptionalDeserializer optionalDeserializer = new OptionalDeserializer(typeProcessor, JustReturn.instance());
models.put(cachedItem, optionalDeserializer);
return optionalDeserializer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,9 @@ private ModelSerializer createMapSerializer(LinkedList<Type> 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);
Expand All @@ -313,7 +315,8 @@ private ModelSerializer createArraySerializer(LinkedList<Type> 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);
Expand All @@ -325,7 +328,8 @@ private ModelSerializer createGenericArraySerializer(LinkedList<Type> 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);
Expand All @@ -339,7 +343,9 @@ private ModelSerializer createOptionalSerializer(LinkedList<Type> 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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:[email protected]">James R. Perkins</a>
*/
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<TestInterface> {
@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<String, TestInterface> map;
}

public static class ListContainer {
public List<TestInterface> list;
}

public static class ArrayContainer {
public TestInterface[] array;
}

public static class OptionalContainer {
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public Optional<TestInterface> 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);
}
}
}
Loading