Skip to content
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

[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

Open
wants to merge 1 commit 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
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand All @@ -14,9 +14,12 @@

import java.util.Map;

import jakarta.json.bind.serializer.JsonbSerializer;
import jakarta.json.stream.JsonGenerator;

import org.eclipse.yasson.internal.JsonbContext;
import org.eclipse.yasson.internal.SerializationContextImpl;
import org.eclipse.yasson.internal.components.ComponentBindings;
import org.eclipse.yasson.internal.serializer.types.TypeSerializers;

/**
Expand All @@ -40,9 +43,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 +88,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
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand Down 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
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand Down 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();
}

/**
* Create new type serializer.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand All @@ -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));
}
}
Loading