[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));
+    }
 }