Skip to content

Commit 7607387

Browse files
committed
Fixed #663
Using the custom JsonbSerializer even when serializing an already supported Map key
1 parent 5d6af1a commit 7607387

File tree

4 files changed

+113
-8
lines changed

4 files changed

+113
-8
lines changed

Diff for: src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2021, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -14,9 +14,12 @@
1414

1515
import java.util.Map;
1616

17+
import jakarta.json.bind.serializer.JsonbSerializer;
1718
import jakarta.json.stream.JsonGenerator;
1819

20+
import org.eclipse.yasson.internal.JsonbContext;
1921
import org.eclipse.yasson.internal.SerializationContextImpl;
22+
import org.eclipse.yasson.internal.components.ComponentBindings;
2023
import org.eclipse.yasson.internal.serializer.types.TypeSerializers;
2124

2225
/**
@@ -40,9 +43,15 @@ ModelSerializer getValueSerializer() {
4043
return valueSerializer;
4144
}
4245

43-
static MapSerializer create(Class<?> keyClass, ModelSerializer keySerializer, ModelSerializer valueSerializer) {
46+
static MapSerializer create(Class<?> keyClass, ModelSerializer keySerializer, ModelSerializer valueSerializer, JsonbContext jsonbContext) {
4447
if (TypeSerializers.isSupportedMapKey(keyClass)) {
45-
return new StringKeyMapSerializer(keySerializer, valueSerializer);
48+
//Issue #663: A custom JsonbSerializer is available for an already supported Map key. Serialization must
49+
//not use normal key:value map. No further checking needed. Wrapping object needs to be used.
50+
if (TypeSerializers.hasCustomJsonbSerializer(keyClass, jsonbContext)) {
51+
return new ObjectKeyMapSerializer(keySerializer, valueSerializer);
52+
} else {
53+
return new StringKeyMapSerializer(keySerializer, valueSerializer);
54+
}
4655
} else if (Object.class.equals(keyClass)) {
4756
return new DynamicMapSerializer(keySerializer, valueSerializer);
4857
}
@@ -79,7 +88,17 @@ public void serialize(Object value, JsonGenerator generator, SerializationContex
7988
}
8089
Class<?> keyClass = key.getClass();
8190
if (TypeSerializers.isSupportedMapKey(keyClass)) {
82-
continue;
91+
92+
//Issue #663: A custom JsonbSerializer is available for an already supported Map key.
93+
//Serialization must not use normal key:value map. No further checking needed. Wrapping object
94+
//needs to be used.
95+
if (TypeSerializers.hasCustomJsonbSerializer(keyClass, context.getJsonbContext())) {
96+
suitable = false;
97+
break;
98+
}
99+
else {
100+
continue;
101+
}
83102
}
84103
//No other checks needed. Map is not suitable for normal key:value map. Wrapping object needs to be used.
85104
suitable = false;

Diff for: src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2021, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -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);

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2021, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -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
*

Diff for: src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java

+75-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2022 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2019, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -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)