Skip to content

[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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ private ModelSerializer createMapSerializer(LinkedList<Type> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be passing the propertyCustomization parameter from SerializationModelCreator.createMapSerializer() as the null parameter here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, not sure here... This method is only to determine if there's a custom Serializer at all...

}

/**
* Create new type serializer.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -851,6 +856,26 @@ public Locale deserialize(JsonParser parser, DeserializationContext ctx, Type rt
}
}

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;
Expand Down Expand Up @@ -934,4 +959,53 @@ public void testMapLocaleString() {
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));
}
}