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 9b283fbd..891df112 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 @@ ModelSerializer getValueSerializer() { 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 @@ public void serialize(Object value, JsonGenerator generator, SerializationContex } 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 522519b3..0379c555 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 @@ private ModelSerializer createMapSerializer(LinkedList chain, Type type, C 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 c25fc897..61031d07 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; @@ -153,6 +154,17 @@ public static boolean isSupportedMapKey(Class clazz) { return Enum.class.isAssignableFrom(clazz) || SUPPORTED_MAP_KEYS.contains(clazz); } + /** + * 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. * diff --git a/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java b/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java index 3961dd2a..36131c24 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 Locale deserialize(JsonParser parser, DeserializationContext ctx, Type rt } } + public static class LocalDateSerializer implements JsonbSerializer { + + 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 { + + 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 { private Map values; @@ -934,4 +959,53 @@ public void testMapLocaleString() { MapObjectLocaleString resObject = jsonb.fromJson(json, MapObjectLocaleString.class); assertEquals(mapObject, resObject); } + + public static class MapObjectLocalDateString extends MapObject {}; + + 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)); + } }