[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
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java
index 9b283fb..891df11 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java
@@ -16,6 +16,7 @@
import jakarta.json.stream.JsonGenerator;
+import org.eclipse.yasson.internal.JsonbContext;
import org.eclipse.yasson.internal.SerializationContextImpl;
import org.eclipse.yasson.internal.serializer.types.TypeSerializers;
@@ -40,9 +41,15 @@
return valueSerializer;
}
- static MapSerializer create(Class<?> keyClass, ModelSerializer keySerializer, ModelSerializer valueSerializer) {
+ static MapSerializer create(Class<?> keyClass, ModelSerializer keySerializer, ModelSerializer valueSerializer, JsonbContext jsonbContext) {
if (TypeSerializers.isSupportedMapKey(keyClass)) {
- return new StringKeyMapSerializer(keySerializer, valueSerializer);
+ //Issue #663: A custom JsonbSerializer is available for an already supported Map key. Serialization must
+ //not use normal key:value map. No further checking needed. Wrapping object needs to be used.
+ if (TypeSerializers.hasCustomJsonbSerializer(keyClass, jsonbContext)) {
+ return new ObjectKeyMapSerializer(keySerializer, valueSerializer);
+ } else {
+ return new StringKeyMapSerializer(keySerializer, valueSerializer);
+ }
} else if (Object.class.equals(keyClass)) {
return new DynamicMapSerializer(keySerializer, valueSerializer);
}
@@ -79,7 +86,17 @@
}
Class<?> keyClass = key.getClass();
if (TypeSerializers.isSupportedMapKey(keyClass)) {
- continue;
+
+ //Issue #663: A custom JsonbSerializer is available for an already supported Map key.
+ //Serialization must not use normal key:value map. No further checking needed. Wrapping object
+ //needs to be used.
+ if (TypeSerializers.hasCustomJsonbSerializer(keyClass, context.getJsonbContext())) {
+ suitable = false;
+ break;
+ }
+ else {
+ continue;
+ }
}
//No other checks needed. Map is not suitable for normal key:value map. Wrapping object needs to be used.
suitable = false;
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 522519b..0379c55 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java
@@ -303,7 +303,7 @@
Class<?> rawClass = ReflectionUtils.getRawType(resolvedKey);
ModelSerializer keySerializer = memberSerializer(chain, keyType, ClassCustomization.empty(), true);
ModelSerializer valueSerializer = memberSerializer(chain, valueType, propertyCustomization, false);
- MapSerializer mapSerializer = MapSerializer.create(rawClass, keySerializer, valueSerializer);
+ MapSerializer mapSerializer = MapSerializer.create(rawClass, keySerializer, valueSerializer, jsonbContext);
KeyWriter keyWriter = new KeyWriter(mapSerializer);
NullVisibilitySwitcher nullVisibilitySwitcher = new NullVisibilitySwitcher(true, keyWriter);
return new NullSerializer(nullVisibilitySwitcher, propertyCustomization, jsonbContext);
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/types/TypeSerializers.java b/src/main/java/org/eclipse/yasson/internal/serializer/types/TypeSerializers.java
index c25fc89..61031d0 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/types/TypeSerializers.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/types/TypeSerializers.java
@@ -54,6 +54,7 @@
import jakarta.json.JsonValue;
import jakarta.json.bind.JsonbException;
+import jakarta.json.bind.serializer.JsonbSerializer;
import org.eclipse.yasson.internal.JsonbContext;
import org.eclipse.yasson.internal.model.customization.Customization;
import org.eclipse.yasson.internal.serializer.ModelSerializer;
@@ -154,6 +155,17 @@
}
/**
+ * Whether type has a custom {@link JsonbSerializer} implementation.
+ *
+ * @param clazz type to serialize
+ * @param jsonbContext jsonb context
+ * @return whether a custom JsonSerializer for the type is available
+ */
+ public static boolean hasCustomJsonbSerializer(Class<?> clazz, JsonbContext jsonbContext) {
+ return jsonbContext.getComponentMatcher().getSerializerBinding(clazz, null).isPresent();
+ }
+
+ /**
* Create new type serializer.
*
* @param clazz type of the serializer
diff --git a/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java b/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java
index 3961dd2..36131c2 100644
--- a/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java
+++ b/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java
@@ -13,12 +13,17 @@
package org.eclipse.yasson.serializers;
import org.junit.jupiter.api.*;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import java.io.StringReader;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
@@ -851,6 +856,26 @@
}
}
+ public static class LocalDateSerializer implements JsonbSerializer<LocalDate> {
+
+ private static final DateTimeFormatter SHORT_FORMAT = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
+
+ @Override
+ public void serialize(LocalDate obj, JsonGenerator generator, SerializationContext ctx) {
+ generator.write(SHORT_FORMAT.format(obj));
+ }
+ }
+
+ public static class LocalDateDeserializer implements JsonbDeserializer<LocalDate> {
+
+ private static final DateTimeFormatter SHORT_FORMAT = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
+
+ @Override
+ public LocalDate deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+ return LocalDate.parse(parser.getString(), SHORT_FORMAT);
+ }
+ }
+
public static class MapObject<K, V> {
private Map<K, V> values;
@@ -934,4 +959,53 @@
MapObjectLocaleString resObject = jsonb.fromJson(json, MapObjectLocaleString.class);
assertEquals(mapObject, resObject);
}
+
+ public static class MapObjectLocalDateString extends MapObject<LocalDate, String> {};
+
+ private void verifyMapObjectCustomLocalDateStringSerialization(JsonObject jsonObject, MapObjectLocalDateString mapObject) {
+
+ // Expected serialization is: {"values":[{"key":"short-local-date","value":"string"},...]}
+ assertEquals(1, jsonObject.size());
+ assertNotNull(jsonObject.get("values"));
+ assertEquals(JsonValue.ValueType.ARRAY, jsonObject.get("values").getValueType());
+ JsonArray jsonArray = jsonObject.getJsonArray("values");
+ assertEquals(mapObject.getValues().size(), jsonArray.size());
+ MapObjectLocalDateString resObject = new MapObjectLocalDateString();
+ for (JsonValue jsonValue : jsonArray) {
+ assertEquals(JsonValue.ValueType.OBJECT, jsonValue.getValueType());
+ JsonObject entry = jsonValue.asJsonObject();
+ assertEquals(2, entry.size());
+ assertNotNull(entry.get("key"));
+ assertEquals(JsonValue.ValueType.STRING, entry.get("key").getValueType());
+ assertNotNull(entry.get("value"));
+ assertEquals(JsonValue.ValueType.STRING, entry.get("value").getValueType());
+ resObject.getValues().put(LocalDate.parse(entry.getString("key"), DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)), entry.getString("value"));
+ }
+ assertEquals(mapObject, resObject);
+ }
+
+ /**
+ * Test for issue #663...
+ * Test a LocalDate/String map as member in a custom class, using a custom LocalDate serializer and deserializer,
+ * even though there's a build-in {@link org.eclipse.yasson.internal.serializer.types.TypeSerializers#isSupportedMapKey(Class)}
+ */
+ @Test
+ public void testMapLocalDateKeyStringValueAsMember() {
+ Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
+ .withSerializers(new LocalDateSerializer())
+ .withDeserializers(new LocalDateDeserializer()));
+
+ MapObjectLocalDateString mapObject = new MapObjectLocalDateString();
+ mapObject.getValues().put(LocalDate.now(), "today");
+ mapObject.getValues().put(LocalDate.now().plusDays(1), "tomorrow");
+
+ String json = jsonb.toJson(mapObject);
+
+ JsonObject jsonObject = Json.createReader(new StringReader(json)).read().asJsonObject();
+ verifyMapObjectCustomLocalDateStringSerialization(jsonObject, mapObject);
+ MapObjectLocalDateString resObject = jsonb.fromJson(json, MapObjectLocalDateString.class);
+ assertEquals(mapObject, resObject);
+ // ensure the keys are of type java.time.LocalDate
+ assertThat(resObject.getValues().keySet().iterator().next(), instanceOf(LocalDate.class));
+ }
}