Skip to content

Commit d60c869

Browse files
authored
[663] Yasson 3.0.3 - Serialization of a Map fails if the key is of a type implemented as SupportedMapKey and using a csutom Serializer (#664)
Using the custom JsonbSerializer even when serializing an already supported Map key
1 parent abde7ec commit d60c869

File tree

4 files changed

+107
-4
lines changed

4 files changed

+107
-4
lines changed

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import jakarta.json.stream.JsonGenerator;
1818

19+
import org.eclipse.yasson.internal.JsonbContext;
1920
import org.eclipse.yasson.internal.SerializationContextImpl;
2021
import org.eclipse.yasson.internal.serializer.types.TypeSerializers;
2122

@@ -40,9 +41,15 @@ ModelSerializer getValueSerializer() {
4041
return valueSerializer;
4142
}
4243

43-
static MapSerializer create(Class<?> keyClass, ModelSerializer keySerializer, ModelSerializer valueSerializer) {
44+
static MapSerializer create(Class<?> keyClass, ModelSerializer keySerializer, ModelSerializer valueSerializer, JsonbContext jsonbContext) {
4445
if (TypeSerializers.isSupportedMapKey(keyClass)) {
45-
return new StringKeyMapSerializer(keySerializer, valueSerializer);
46+
//Issue #663: A custom JsonbSerializer is available for an already supported Map key. Serialization must
47+
//not use normal key:value map. No further checking needed. Wrapping object needs to be used.
48+
if (TypeSerializers.hasCustomJsonbSerializer(keyClass, jsonbContext)) {
49+
return new ObjectKeyMapSerializer(keySerializer, valueSerializer);
50+
} else {
51+
return new StringKeyMapSerializer(keySerializer, valueSerializer);
52+
}
4653
} else if (Object.class.equals(keyClass)) {
4754
return new DynamicMapSerializer(keySerializer, valueSerializer);
4855
}
@@ -79,7 +86,17 @@ public void serialize(Object value, JsonGenerator generator, SerializationContex
7986
}
8087
Class<?> keyClass = key.getClass();
8188
if (TypeSerializers.isSupportedMapKey(keyClass)) {
82-
continue;
89+
90+
//Issue #663: A custom JsonbSerializer is available for an already supported Map key.
91+
//Serialization must not use normal key:value map. No further checking needed. Wrapping object
92+
//needs to be used.
93+
if (TypeSerializers.hasCustomJsonbSerializer(keyClass, context.getJsonbContext())) {
94+
suitable = false;
95+
break;
96+
}
97+
else {
98+
continue;
99+
}
83100
}
84101
//No other checks needed. Map is not suitable for normal key:value map. Wrapping object needs to be used.
85102
suitable = false;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ private ModelSerializer createMapSerializer(LinkedList<Type> chain, Type type, C
303303
Class<?> rawClass = ReflectionUtils.getRawType(resolvedKey);
304304
ModelSerializer keySerializer = memberSerializer(chain, keyType, ClassCustomization.empty(), true);
305305
ModelSerializer valueSerializer = memberSerializer(chain, valueType, propertyCustomization, false);
306-
MapSerializer mapSerializer = MapSerializer.create(rawClass, keySerializer, valueSerializer);
306+
MapSerializer mapSerializer = MapSerializer.create(rawClass, keySerializer, valueSerializer, jsonbContext);
307307
KeyWriter keyWriter = new KeyWriter(mapSerializer);
308308
NullVisibilitySwitcher nullVisibilitySwitcher = new NullVisibilitySwitcher(true, keyWriter);
309309
return new NullSerializer(nullVisibilitySwitcher, propertyCustomization, jsonbContext);

src/main/java/org/eclipse/yasson/internal/serializer/types/TypeSerializers.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import jakarta.json.JsonValue;
5555
import jakarta.json.bind.JsonbException;
5656

57+
import jakarta.json.bind.serializer.JsonbSerializer;
5758
import org.eclipse.yasson.internal.JsonbContext;
5859
import org.eclipse.yasson.internal.model.customization.Customization;
5960
import org.eclipse.yasson.internal.serializer.ModelSerializer;
@@ -153,6 +154,17 @@ public static boolean isSupportedMapKey(Class<?> clazz) {
153154
return Enum.class.isAssignableFrom(clazz) || SUPPORTED_MAP_KEYS.contains(clazz);
154155
}
155156

157+
/**
158+
* Whether type has a custom {@link JsonbSerializer} implementation.
159+
*
160+
* @param clazz type to serialize
161+
* @param jsonbContext jsonb context
162+
* @return whether a custom JsonSerializer for the type is available
163+
*/
164+
public static boolean hasCustomJsonbSerializer(Class<?> clazz, JsonbContext jsonbContext) {
165+
return jsonbContext.getComponentMatcher().getSerializerBinding(clazz, null).isPresent();
166+
}
167+
156168
/**
157169
* Create new type serializer.
158170
*

src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
1313
package org.eclipse.yasson.serializers;
1414

1515
import org.junit.jupiter.api.*;
16+
import static org.hamcrest.CoreMatchers.instanceOf;
17+
import static org.hamcrest.MatcherAssert.assertThat;
1618
import static org.junit.jupiter.api.Assertions.*;
1719

1820
import java.io.StringReader;
1921
import java.lang.reflect.ParameterizedType;
2022
import java.lang.reflect.Type;
2123
import java.math.BigDecimal;
24+
import java.time.LocalDate;
25+
import java.time.format.DateTimeFormatter;
26+
import java.time.format.FormatStyle;
2227
import java.util.Comparator;
2328
import java.util.HashMap;
2429
import java.util.Locale;
@@ -851,6 +856,26 @@ public Locale deserialize(JsonParser parser, DeserializationContext ctx, Type rt
851856
}
852857
}
853858

859+
public static class LocalDateSerializer implements JsonbSerializer<LocalDate> {
860+
861+
private static final DateTimeFormatter SHORT_FORMAT = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
862+
863+
@Override
864+
public void serialize(LocalDate obj, JsonGenerator generator, SerializationContext ctx) {
865+
generator.write(SHORT_FORMAT.format(obj));
866+
}
867+
}
868+
869+
public static class LocalDateDeserializer implements JsonbDeserializer<LocalDate> {
870+
871+
private static final DateTimeFormatter SHORT_FORMAT = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
872+
873+
@Override
874+
public LocalDate deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
875+
return LocalDate.parse(parser.getString(), SHORT_FORMAT);
876+
}
877+
}
878+
854879
public static class MapObject<K, V> {
855880

856881
private Map<K, V> values;
@@ -934,4 +959,53 @@ public void testMapLocaleString() {
934959
MapObjectLocaleString resObject = jsonb.fromJson(json, MapObjectLocaleString.class);
935960
assertEquals(mapObject, resObject);
936961
}
962+
963+
public static class MapObjectLocalDateString extends MapObject<LocalDate, String> {};
964+
965+
private void verifyMapObjectCustomLocalDateStringSerialization(JsonObject jsonObject, MapObjectLocalDateString mapObject) {
966+
967+
// Expected serialization is: {"values":[{"key":"short-local-date","value":"string"},...]}
968+
assertEquals(1, jsonObject.size());
969+
assertNotNull(jsonObject.get("values"));
970+
assertEquals(JsonValue.ValueType.ARRAY, jsonObject.get("values").getValueType());
971+
JsonArray jsonArray = jsonObject.getJsonArray("values");
972+
assertEquals(mapObject.getValues().size(), jsonArray.size());
973+
MapObjectLocalDateString resObject = new MapObjectLocalDateString();
974+
for (JsonValue jsonValue : jsonArray) {
975+
assertEquals(JsonValue.ValueType.OBJECT, jsonValue.getValueType());
976+
JsonObject entry = jsonValue.asJsonObject();
977+
assertEquals(2, entry.size());
978+
assertNotNull(entry.get("key"));
979+
assertEquals(JsonValue.ValueType.STRING, entry.get("key").getValueType());
980+
assertNotNull(entry.get("value"));
981+
assertEquals(JsonValue.ValueType.STRING, entry.get("value").getValueType());
982+
resObject.getValues().put(LocalDate.parse(entry.getString("key"), DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)), entry.getString("value"));
983+
}
984+
assertEquals(mapObject, resObject);
985+
}
986+
987+
/**
988+
* Test for issue #663...
989+
* Test a LocalDate/String map as member in a custom class, using a custom LocalDate serializer and deserializer,
990+
* even though there's a build-in {@link org.eclipse.yasson.internal.serializer.types.TypeSerializers#isSupportedMapKey(Class)}
991+
*/
992+
@Test
993+
public void testMapLocalDateKeyStringValueAsMember() {
994+
Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
995+
.withSerializers(new LocalDateSerializer())
996+
.withDeserializers(new LocalDateDeserializer()));
997+
998+
MapObjectLocalDateString mapObject = new MapObjectLocalDateString();
999+
mapObject.getValues().put(LocalDate.now(), "today");
1000+
mapObject.getValues().put(LocalDate.now().plusDays(1), "tomorrow");
1001+
1002+
String json = jsonb.toJson(mapObject);
1003+
1004+
JsonObject jsonObject = Json.createReader(new StringReader(json)).read().asJsonObject();
1005+
verifyMapObjectCustomLocalDateStringSerialization(jsonObject, mapObject);
1006+
MapObjectLocalDateString resObject = jsonb.fromJson(json, MapObjectLocalDateString.class);
1007+
assertEquals(mapObject, resObject);
1008+
// ensure the keys are of type java.time.LocalDate
1009+
assertThat(resObject.getValues().keySet().iterator().next(), instanceOf(LocalDate.class));
1010+
}
9371011
}

0 commit comments

Comments
 (0)