Skip to content

Commit 0804c14

Browse files
committed
[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 <[email protected]>
1 parent 40c0444 commit 0804c14

File tree

4 files changed

+382
-23
lines changed

4 files changed

+382
-23
lines changed

src/main/java/org/eclipse/yasson/internal/deserializer/DeserializationModelCreator.java

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,15 @@ private ModelDeserializer<JsonParser> deserializerChainInternal(LinkedList<Type>
180180
return typeDeserializer;
181181
}
182182
if (Collection.class.isAssignableFrom(rawType)) {
183-
return createCollectionDeserializer(cachedItem, rawType, chain, propertyCustomization);
183+
return createCollectionDeserializer(cachedItem, rawType, chain);
184184
} else if (Map.class.isAssignableFrom(rawType)) {
185-
return createMapDeserializer(cachedItem, rawType, chain, propertyCustomization);
185+
return createMapDeserializer(cachedItem, rawType, chain);
186186
} else if (rawType.isArray()) {
187-
return createArrayDeserializer(cachedItem, rawType, chain, propertyCustomization);
187+
return createArrayDeserializer(cachedItem, rawType, chain);
188188
} else if (type instanceof GenericArrayType) {
189-
return createGenericArray(cachedItem, rawType, chain, propertyCustomization);
189+
return createGenericArray(cachedItem, rawType, chain);
190190
} else if (Optional.class.isAssignableFrom(rawType)) {
191-
return createOptionalDeserializer(chain, type, propertyCustomization, cachedItem);
191+
return createOptionalDeserializer(chain, type, cachedItem);
192192
} else {
193193
return createObjectDeserializer(chain, type, propertyCustomization, classModel, rawType, cachedItem);
194194
}
@@ -262,8 +262,7 @@ private ModelDeserializer<JsonParser> createObjectDeserializer(LinkedList<Type>
262262

263263
private ModelDeserializer<JsonParser> createCollectionDeserializer(CachedItem cachedItem,
264264
Class<?> rawType,
265-
LinkedList<Type> chain,
266-
Customization propertyCustomization) {
265+
LinkedList<Type> chain) {
267266
Type type = cachedItem.type;
268267
Type colType = type instanceof ParameterizedType
269268
? ((ParameterizedType) type).getActualTypeArguments()[0]
@@ -284,8 +283,7 @@ private ModelDeserializer<JsonParser> createCollectionDeserializer(CachedItem ca
284283

285284
private ModelDeserializer<JsonParser> createMapDeserializer(CachedItem cachedItem,
286285
Class<?> rawType,
287-
LinkedList<Type> chain,
288-
Customization propertyCustomization) {
286+
LinkedList<Type> chain) {
289287
Type type = cachedItem.type;
290288
Type keyType = type instanceof ParameterizedType
291289
? ((ParameterizedType) type).getActualTypeArguments()[0]
@@ -298,9 +296,10 @@ private ModelDeserializer<JsonParser> createMapDeserializer(CachedItem cachedIte
298296
ClassCustomization.empty(),
299297
JustReturn.instance(),
300298
MAP_KEY_EVENTS);
299+
ClassModel valueClassModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(valueType));
301300
ModelDeserializer<JsonParser> valueProcessor = typeProcessor(chain,
302301
valueType,
303-
propertyCustomization,
302+
valueClassModel.getClassCustomization(),
304303
JustReturn.instance());
305304

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

316315
private ModelDeserializer<JsonParser> createArrayDeserializer(CachedItem cachedItem,
317316
Class<?> rawType,
318-
LinkedList<Type> chain,
319-
Customization propertyCustomization) {
317+
LinkedList<Type> chain) {
320318
JsonbConfigProperties configProperties = jsonbContext.getConfigProperties();
321319
if (rawType.equals(byte[].class) && !configProperties.getBinaryDataStrategy().equals(BinaryDataStrategy.BYTE)) {
322320
String strategy = configProperties.getBinaryDataStrategy();
321+
// Special case for byte[] with base64 encoding - use String's class customization
322+
ClassModel stringModel = jsonbContext.getMappingContext().getOrCreateClassModel(String.class);
323323
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain,
324324
String.class,
325-
propertyCustomization,
325+
stringModel.getClassCustomization(),
326326
JustReturn.instance());
327327
ModelDeserializer<JsonParser> base64Deserializer = ArrayInstanceCreator.createBase64Deserializer(strategy,
328328
typeProcessor);
@@ -331,22 +331,23 @@ private ModelDeserializer<JsonParser> createArrayDeserializer(CachedItem cachedI
331331
return nullChecker;
332332
}
333333
Class<?> arrayType = rawType.getComponentType();
334+
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(arrayType);
334335
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain,
335336
arrayType,
336-
propertyCustomization,
337+
classModel.getClassCustomization(),
337338
JustReturn.instance());
338339
return createArrayCommonDeserializer(cachedItem, rawType, arrayType, typeProcessor);
339340
}
340341

341342
private ModelDeserializer<JsonParser> createGenericArray(CachedItem cachedItem,
342343
Class<?> rawType,
343-
LinkedList<Type> chain,
344-
Customization propertyCustomization) {
344+
LinkedList<Type> chain) {
345345
GenericArrayType type = (GenericArrayType) cachedItem.type;
346346
Class<?> component = ReflectionUtils.getRawType(type.getGenericComponentType());
347+
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(component);
347348
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain,
348349
type.getGenericComponentType(),
349-
propertyCustomization,
350+
classModel.getClassCustomization(),
350351
JustReturn.instance());
351352
return createArrayCommonDeserializer(cachedItem, rawType, component, typeProcessor);
352353
}
@@ -365,12 +366,13 @@ private ModelDeserializer<JsonParser> createArrayCommonDeserializer(CachedItem c
365366

366367
private OptionalDeserializer createOptionalDeserializer(LinkedList<Type> chain,
367368
Type type,
368-
Customization propertyCustomization,
369369
CachedItem cachedItem) {
370370
Type colType = type instanceof ParameterizedType
371371
? ((ParameterizedType) type).getActualTypeArguments()[0]
372372
: Object.class;
373-
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain, colType, propertyCustomization, JustReturn.instance());
373+
colType = ReflectionUtils.resolveType(chain, colType);
374+
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(colType));
375+
ModelDeserializer<JsonParser> typeProcessor = typeProcessor(chain, colType, classModel.getClassCustomization(), JustReturn.instance());
374376
OptionalDeserializer optionalDeserializer = new OptionalDeserializer(typeProcessor, JustReturn.instance());
375377
models.put(cachedItem, optionalDeserializer);
376378
return optionalDeserializer;

src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,9 @@ private ModelSerializer createMapSerializer(LinkedList<Type> chain, Type type, C
302302
Type resolvedKey = ReflectionUtils.resolveType(chain, keyType);
303303
Class<?> rawClass = ReflectionUtils.getRawType(resolvedKey);
304304
ModelSerializer keySerializer = memberSerializer(chain, keyType, ClassCustomization.empty(), true);
305-
ModelSerializer valueSerializer = memberSerializer(chain, valueType, propertyCustomization, false);
305+
Type resolvedValue = ReflectionUtils.resolveType(chain, valueType);
306+
ClassModel valueClassModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(resolvedValue));
307+
ModelSerializer valueSerializer = memberSerializer(chain, valueType, valueClassModel.getClassCustomization(), false);
306308
MapSerializer mapSerializer = MapSerializer.create(rawClass, keySerializer, valueSerializer, jsonbContext);
307309
KeyWriter keyWriter = new KeyWriter(mapSerializer);
308310
NullVisibilitySwitcher nullVisibilitySwitcher = new NullVisibilitySwitcher(true, keyWriter);
@@ -313,7 +315,8 @@ private ModelSerializer createArraySerializer(LinkedList<Type> chain,
313315
Class<?> raw,
314316
Customization propertyCustomization) {
315317
Class<?> arrayComponent = raw.getComponentType();
316-
ModelSerializer modelSerializer = memberSerializer(chain, arrayComponent, propertyCustomization, false);
318+
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(arrayComponent);
319+
ModelSerializer modelSerializer = memberSerializer(chain, arrayComponent, classModel.getClassCustomization(), false);
317320
ModelSerializer arraySerializer = ArraySerializer.create(raw, jsonbContext, modelSerializer);
318321
KeyWriter keyWriter = new KeyWriter(arraySerializer);
319322
NullVisibilitySwitcher nullVisibilitySwitcher = new NullVisibilitySwitcher(true, keyWriter);
@@ -325,7 +328,8 @@ private ModelSerializer createGenericArraySerializer(LinkedList<Type> chain,
325328
Customization propertyCustomization) {
326329
Class<?> raw = ReflectionUtils.getRawType(type);
327330
Class<?> component = ReflectionUtils.getRawType(((GenericArrayType) type).getGenericComponentType());
328-
ModelSerializer modelSerializer = memberSerializer(chain, component, propertyCustomization, false);
331+
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(component);
332+
ModelSerializer modelSerializer = memberSerializer(chain, component, classModel.getClassCustomization(), false);
329333
ModelSerializer arraySerializer = ArraySerializer.create(raw, jsonbContext, modelSerializer);
330334
KeyWriter keyWriter = new KeyWriter(arraySerializer);
331335
NullVisibilitySwitcher nullVisibilitySwitcher = new NullVisibilitySwitcher(true, keyWriter);
@@ -339,7 +343,9 @@ private ModelSerializer createOptionalSerializer(LinkedList<Type> chain,
339343
Type optType = type instanceof ParameterizedType
340344
? ((ParameterizedType) type).getActualTypeArguments()[0]
341345
: Object.class;
342-
ModelSerializer modelSerializer = memberSerializer(chain, optType, propertyCustomization, isKey);
346+
Type resolvedOptType = ReflectionUtils.resolveType(chain, optType);
347+
ClassModel classModel = jsonbContext.getMappingContext().getOrCreateClassModel(ReflectionUtils.getRawType(resolvedOptType));
348+
ModelSerializer modelSerializer = memberSerializer(chain, optType, classModel.getClassCustomization(), isKey);
343349
return new OptionalSerializer(modelSerializer);
344350
}
345351

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* Copyright (c) 2025 IBM and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0,
7+
* or the Eclipse Distribution License v. 1.0 which is available at
8+
* http://www.eclipse.org/org/documents/edl-v10.php.
9+
*
10+
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
11+
*/
12+
13+
package org.eclipse.yasson.serializers;
14+
15+
import java.lang.reflect.Type;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Optional;
20+
21+
import jakarta.json.bind.Jsonb;
22+
import jakarta.json.bind.JsonbBuilder;
23+
import jakarta.json.bind.JsonbConfig;
24+
import jakarta.json.bind.annotation.JsonbTypeDeserializer;
25+
import jakarta.json.bind.config.BinaryDataStrategy;
26+
import jakarta.json.bind.serializer.DeserializationContext;
27+
import jakarta.json.bind.serializer.JsonbDeserializer;
28+
import jakarta.json.stream.JsonParser;
29+
import org.junit.jupiter.api.AfterEach;
30+
import org.junit.jupiter.api.Assertions;
31+
import org.junit.jupiter.api.BeforeEach;
32+
import org.junit.jupiter.api.Test;
33+
34+
/**
35+
* Tests that {@link jakarta.json.bind.annotation.JsonbTypeDeserializer @JsonbTypeDeserializer} annotated types are
36+
* properly detected and used when those types are used as elements/values in containers (Maps, Collections,
37+
* Arrays, Optionals).
38+
*
39+
* @author <a href="mailto:[email protected]">James R. Perkins</a>
40+
*/
41+
public class TypeDeserializerOnContainersTest {
42+
43+
// Test interface with type-level deserializer annotation
44+
@JsonbTypeDeserializer(TestInterfaceDeserializer.class)
45+
public interface TestInterface {
46+
String getValue();
47+
}
48+
49+
// Implementation of the test interface
50+
public static class TestImpl implements TestInterface {
51+
private final String value;
52+
53+
public TestImpl(final String value) {
54+
this.value = value;
55+
}
56+
57+
@Override
58+
public String getValue() {
59+
return value;
60+
}
61+
}
62+
63+
// Custom deserializer for TestInterface
64+
public static class TestInterfaceDeserializer implements JsonbDeserializer<TestInterface> {
65+
@Override
66+
public TestInterface deserialize(final JsonParser parser, final DeserializationContext ctx, final Type rtType) {
67+
// Parse the JSON object to get the value field
68+
Assertions.assertTrue(parser.hasNext(), "Expected the key name");
69+
parser.next();
70+
Assertions.assertTrue(parser.hasNext(), "Expected the value");
71+
parser.next();
72+
final String value = parser.getString();
73+
Assertions.assertTrue(parser.hasNext(), "Expected the end of an object");
74+
parser.next();
75+
return new TestImpl("DESERIALIZED:" + value);
76+
}
77+
}
78+
79+
// Container classes for testing
80+
public static class MapContainer {
81+
public Map<String, TestInterface> map;
82+
}
83+
84+
public static class ListContainer {
85+
public List<TestInterface> list;
86+
}
87+
88+
public static class ArrayContainer {
89+
public TestInterface[] array;
90+
}
91+
92+
public static class OptionalContainer {
93+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
94+
public Optional<TestInterface> optional;
95+
}
96+
97+
public static class ByteArrayContainer {
98+
public byte[] data;
99+
}
100+
101+
private Jsonb jsonb;
102+
103+
@BeforeEach
104+
public void createJsonb() {
105+
// Create a new Jsonb for each test to avoid type caching
106+
jsonb = JsonbBuilder.create();
107+
}
108+
109+
@AfterEach
110+
public void closeJsonb() throws Exception {
111+
if (jsonb != null) {
112+
jsonb.close();
113+
}
114+
}
115+
116+
@Test
117+
public void testTypeDeserializerOnMapValues() {
118+
final String json = "{\"map\":{\"key1\":{\"value\":\"value1\"},\"key2\":{\"value\":\"value2\"}}}";
119+
120+
final MapContainer result = jsonb.fromJson(json, MapContainer.class);
121+
122+
Assertions.assertNotNull(result.map);
123+
Assertions.assertEquals(2, result.map.size(), () -> String.format("Expected two entries got %s", result.map));
124+
Assertions.assertEquals("DESERIALIZED:value1", result.map.get("key1").getValue());
125+
Assertions.assertEquals("DESERIALIZED:value2", result.map.get("key2").getValue());
126+
}
127+
128+
@Test
129+
public void testTypeDeserializerOnListElements() {
130+
final String json = "{\"list\":[{\"value\":\"value1\"},{\"value\":\"value2\"}]}";
131+
132+
final ListContainer result = jsonb.fromJson(json, ListContainer.class);
133+
134+
Assertions.assertNotNull(result.list);
135+
Assertions.assertEquals(2, result.list.size(), () -> String.format("Expected two entries got %s", result.list));
136+
Assertions.assertEquals("DESERIALIZED:value1", result.list.get(0).getValue());
137+
Assertions.assertEquals("DESERIALIZED:value2", result.list.get(1).getValue());
138+
}
139+
140+
@Test
141+
public void testTypeDeserializerOnArrayElements() {
142+
final String json = "{\"array\":[{\"value\":\"value1\"},{\"value\":\"value2\"}]}";
143+
144+
final ArrayContainer result = jsonb.fromJson(json, ArrayContainer.class);
145+
146+
Assertions.assertNotNull(result.array);
147+
Assertions.assertEquals(2, result.array.length, () -> String.format("Expected two entries got %s", Arrays.toString(result.array)));
148+
Assertions.assertEquals("DESERIALIZED:value1", result.array[0].getValue());
149+
Assertions.assertEquals("DESERIALIZED:value2", result.array[1].getValue());
150+
}
151+
152+
@Test
153+
public void testTypeDeserializerOnOptionalValue() {
154+
final String json = "{\"optional\":{\"value\":\"value1\"}}";
155+
156+
final OptionalContainer result = jsonb.fromJson(json, OptionalContainer.class);
157+
158+
Assertions.assertNotNull(result.optional);
159+
Assertions.assertTrue(result.optional.isPresent(), "Expected value to be present, but the optional was empty.");
160+
Assertions.assertEquals("DESERIALIZED:value1", result.optional.get().getValue());
161+
}
162+
163+
@Test
164+
public void testTypeDeserializerOnByteArray() {
165+
final String json = "{\"data\":[1,2,3,4,5]}";
166+
167+
final ByteArrayContainer result = jsonb.fromJson(json, ByteArrayContainer.class);
168+
169+
Assertions.assertNotNull(result.data);
170+
Assertions.assertEquals(5, result.data.length);
171+
Assertions.assertArrayEquals(new byte[]{1, 2, 3, 4, 5}, result.data);
172+
}
173+
174+
@Test
175+
public void testTypeDeserializerOnByteArrayWithBase64() throws Exception {
176+
try (Jsonb base64Jsonb = JsonbBuilder.create(new JsonbConfig()
177+
.withBinaryDataStrategy(BinaryDataStrategy.BASE_64))) {
178+
179+
// "SGVsbG8=" is "Hello" in base64
180+
final String json = "{\"data\":\"SGVsbG8=\"}";
181+
182+
final ByteArrayContainer result = base64Jsonb.fromJson(json, ByteArrayContainer.class);
183+
184+
Assertions.assertNotNull(result.data);
185+
Assertions.assertArrayEquals("Hello".getBytes(), result.data);
186+
}
187+
}
188+
}

0 commit comments

Comments
 (0)