Issue #500: Serialization of a Map fails if the key uses a custom Serializer (#501) Signed-off-by: rmartinc <rmartinc@redhat.com>
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 9700b07..e5b8d8e 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java
@@ -12,12 +12,17 @@ package org.eclipse.yasson.internal.serializer; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Iterator; import java.util.Map; +import java.util.Optional; import jakarta.json.bind.serializer.SerializationContext; import jakarta.json.stream.JsonGenerator; +import org.eclipse.yasson.internal.ReflectionUtils; + /** * Serialize {@link Map}. * @@ -91,12 +96,18 @@ private Delegate<K, V> serializer; /** + * Flag to know if the process is for the key (0) or the value (1). + */ + private int actualTypeArgument; + + /** * Creates an instance of {@link Map} serialization. * * @param builder current instance of {@link SerializerBuilder} */ protected MapSerializer(SerializerBuilder builder) { super(builder); + actualTypeArgument = 0; nullable = builder.getJsonbContext().getConfigProperties().getConfigNullable(); forceMapArraySerializerForNullKeys = builder.getJsonbContext().getConfigProperties().isForceMapArraySerializerForNullKeys(); serializer = null; @@ -203,4 +214,35 @@ return nullable; } + /** + * Flag to serialize the key in the map. + */ + protected void serializeKey() { + this.actualTypeArgument = 0; + } + + /** + * Flag to serialize the value in the map. + */ + protected void serializeValue() { + this.actualTypeArgument = 1; + } + + /** + * In a map the type can refer to the key or the value type depending which + * one is currently being processed. The field <em>actualTypeArgument</em> + * controls which one is being serialized at the moment. + * + * @param valueType The value type which should be of type Map<K,V> + * @return The type for the key or the value + */ + @Override + protected Type getValueType(Type valueType) { + if (valueType instanceof ParameterizedType && ((ParameterizedType) valueType).getActualTypeArguments().length > actualTypeArgument) { + Optional<Type> runtimeTypeOptional = ReflectionUtils + .resolveOptionalType(this, ((ParameterizedType) valueType).getActualTypeArguments()[actualTypeArgument]); + return runtimeTypeOptional.orElse(Object.class); + } + return Object.class; + } }
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/MapToEntriesArraySerializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/MapToEntriesArraySerializer.java index 13d0cdc..7feec26 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/MapToEntriesArraySerializer.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/MapToEntriesArraySerializer.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2021 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 @@ -106,8 +106,10 @@ obj.forEach((key, value) -> { generator.writeStartObject(); generator.writeKey(keyEntryName); + serializer.serializeKey(); serializer.serializeItem(key, generator, ctx); generator.writeKey(valueEntryName); + serializer.serializeValue(); serializer.serializeItem(value, generator, ctx); generator.writeEnd(); });
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/MapToObjectSerializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/MapToObjectSerializer.java index bdc88b5..f77c46b 100644 --- a/src/main/java/org/eclipse/yasson/internal/serializer/MapToObjectSerializer.java +++ b/src/main/java/org/eclipse/yasson/internal/serializer/MapToObjectSerializer.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2021 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 @@ -96,6 +96,7 @@ continue; } generator.writeKey(keyString); + serializer.serializeValue(); serializer.serializeItem(value, generator, ctx); } }
diff --git a/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java b/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java index adce79d..eb430ab 100644 --- a/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java +++ b/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2021 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 @@ -21,16 +21,25 @@ import java.math.BigDecimal; import java.util.Comparator; import java.util.HashMap; +import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonObject; +import jakarta.json.JsonValue; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbConfig; +import jakarta.json.bind.serializer.DeserializationContext; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import jakarta.json.bind.serializer.SerializationContext; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; import org.eclipse.yasson.serializers.model.Pokemon; import org.eclipse.yasson.serializers.model.Trainer; @@ -827,4 +836,103 @@ } } + public static class LocaleSerializer implements JsonbSerializer<Locale> { + + @Override + public void serialize(Locale obj, JsonGenerator generator, SerializationContext ctx) { + generator.write(obj.toLanguageTag()); + } + } + + public static class LocaleDeserializer implements JsonbDeserializer<Locale> { + + @Override + public Locale deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { + return Locale.forLanguageTag(parser.getString()); + } + } + + public static class MapObject<K, V> { + + private Map<K, V> values; + + public MapObject() { + this.values = new HashMap<>(); + } + + public Map<K, V> getValues() { + return values; + } + + public void setValues(Map<K, V> values) { + if (values == null) { + throw new IllegalArgumentException("values cannot be null"); + } + this.values = values; + } + + @Override + public boolean equals(Object o) { + if (o instanceof MapObject) { + MapObject<?,?> to = (MapObject<?,?>) o; + return values.equals(to.values); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.values); + } + + @Override + public String toString() { + return values.toString(); + } + } + + public static class MapObjectLocaleString extends MapObject<Locale, String> {}; + + private void verifyMapObjectLocaleStringSerialization(JsonObject jsonObject, MapObjectLocaleString mapObject) { + // Expected serialization is: {"values":[{"key":"lang-tag","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()); + MapObjectLocaleString resObject = new MapObjectLocaleString(); + 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(Locale.forLanguageTag(entry.getString("key")), entry.getString("value")); + } + assertEquals(mapObject, resObject); + } + + /** + * Test a Locale/String map with custom Locale serializer and deserializer. + */ + @Test + public void testMapLocaleString() { + Jsonb jsonb = JsonbBuilder.create(new JsonbConfig() + .withSerializers(new LocaleSerializer()) + .withDeserializers(new LocaleDeserializer())); + + MapObjectLocaleString mapObject = new MapObjectLocaleString(); + mapObject.getValues().put(Locale.US, "us"); + mapObject.getValues().put(Locale.ENGLISH, "en"); + mapObject.getValues().put(Locale.JAPAN, "jp"); + + String json = jsonb.toJson(mapObject); + JsonObject jsonObject = Json.createReader(new StringReader(json)).read().asJsonObject(); + verifyMapObjectLocaleStringSerialization(jsonObject, mapObject); + + MapObjectLocaleString resObject = jsonb.fromJson(json, MapObjectLocaleString.class); + assertEquals(mapObject, resObject); + } }