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