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

Fix enums as map keys #826

Merged
merged 1 commit into from
Apr 22, 2024
Merged
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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jmh = "1.37"
groovy = "4.0.18"

micronaut = "4.4.0"
micronaut-platform = "4.3.8"
micronaut-platform = "4.4.0"
micronaut-docs = "2.0.0"
micronaut-test = "4.2.1"
micronaut-discovery = "4.2.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
package io.micronaut.serde.jackson

import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.json.JsonMapper
import io.micronaut.serde.jackson.tst.AfterCareStatsEntry
import io.micronaut.serde.jackson.tst.ClassificationAndStats
import io.micronaut.serde.jackson.tst.ClassificationVars
import io.micronaut.serde.jackson.tst.MainAggregationVm

abstract class JsonIgnoreSpec extends JsonCompileSpec {

abstract protected String unknownPropertyMessage(String propertyName, String className)

def 'JsonIgnore and enum as map keys'() {
given:
def ctx = ApplicationContext.run()
def jsonMapper = ctx.getBean(JsonMapper)
def obj = new MainAggregationVm(
List.of(
new ClassificationAndStats(
new ClassificationVars("01"),
new AfterCareStatsEntry()
)
)
)
def json = '{"afterCare":[{"klassifisering":{"regionKode":"01"},"stats":{"SomeField1":0,"SomeField2":0}}]}'
expect:
serializeToString(jsonMapper, obj) == json

cleanup:
ctx.close()
}

void 'JsonIgnoreType'() {
given:
def compiled = buildContext('''
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.micronaut.serde.jackson.tst;

import io.micronaut.core.annotation.Introspected;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Introspected
public record AfterCareStatsEntry() implements StatsEntry {

static List<Aggregation> AFTERCARE_AGGREGATIONS = List.of(
Aggregation.FIELD_1,
Aggregation.FIELD_2
);

@Override
public Map<Aggregation, Integer> getShouldNotAppearInJson() {
return AFTERCARE_AGGREGATIONS.stream().collect(Collectors.toMap(it -> it, it -> 0, (x, y) -> y, LinkedHashMap::new));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.micronaut.serde.jackson.tst;

import com.fasterxml.jackson.annotation.JsonValue;
import io.micronaut.core.annotation.Introspected;

@Introspected
public enum Aggregation {

FIELD_1("SomeField1"),
FIELD_2("SomeField2");

private String fieldName;

Aggregation(String fieldName) {
this.fieldName = fieldName;
}

@JsonValue
public String getFieldName() {
return fieldName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.micronaut.serde.jackson.tst;

import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.micronaut.core.annotation.Introspected;

import java.util.Map;

@Introspected
public record ClassificationAndStats<T extends StatsEntry>(
ClassificationVars klassifisering,
/** Ignore field to avoid double wrapping of values in resulting JSON */
@JsonIgnore
T stats
) {
@JsonGetter("stats")
Map<Aggregation, Integer> getValues() {
return stats.getShouldNotAppearInJson();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.micronaut.serde.jackson.tst;

import io.micronaut.core.annotation.Introspected;

@Introspected
public record ClassificationVars(
String regionKode
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.micronaut.serde.jackson.tst;

import io.micronaut.core.annotation.Introspected;

import java.util.List;

@Introspected
public record MainAggregationVm(
List<ClassificationAndStats<AfterCareStatsEntry>> afterCare
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.micronaut.serde.jackson.tst;

import java.util.Map;

public interface StatsEntry {
Map<Aggregation, Integer> getShouldNotAppearInJson();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.micronaut.serde.jackson.annotation


import io.micronaut.core.naming.NameUtils
import io.micronaut.serde.jackson.JsonIgnoreSpec

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@
package io.micronaut.serde.support.serializers;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.beans.exceptions.IntrospectionException;
import io.micronaut.core.convert.exceptions.ConversionErrorException;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.json.tree.JsonNode;
import io.micronaut.serde.Encoder;
import io.micronaut.serde.ObjectSerializer;
import io.micronaut.serde.Serializer;
import io.micronaut.serde.exceptions.SerdeException;
import io.micronaut.serde.support.SerializerRegistrar;
import io.micronaut.serde.support.util.JsonNodeEncoder;
import io.micronaut.serde.util.CustomizableSerializer;

import java.io.IOException;
Expand All @@ -43,8 +46,11 @@ final class CustomizedMapSerializer<K, V> implements CustomizableSerializer<Map<
@Override
public ObjectSerializer<Map<K, V>> createSpecific(EncoderContext context, Argument<? extends Map<K, V>> type) throws SerdeException {
final Argument<?>[] generics = type.getTypeParameters();
final boolean hasGenerics = ArrayUtils.isNotEmpty(generics) && generics.length != 2;
final boolean hasGenerics = ArrayUtils.isNotEmpty(generics) && generics.length == 2;
if (hasGenerics) {
final Argument<K> keyGeneric = (Argument<K>) generics[0];
final Serializer<K> keySerializer = findKeySerializer(context, keyGeneric);
final boolean isStringKey = keyGeneric.getType().equals(String.class) || CharSequence.class.isAssignableFrom(keyGeneric.getType());
final Argument<V> valueGeneric = (Argument<V>) generics[1];
final Serializer<V> valSerializer = (Serializer<V>) context.findSerializer(valueGeneric).createSpecific(context, valueGeneric);
return new ObjectSerializer<>() {
Expand All @@ -58,17 +64,20 @@ public void serialize(Encoder encoder, EncoderContext context, Argument<? extend

@Override
public void serializeInto(Encoder encoder, EncoderContext context, Argument<? extends Map<K, V>> type, Map<K, V> value) throws IOException {
for (K k : value.keySet()) {
encodeMapKey(context, encoder, k);
final V v = value.get(k);
for (Map.Entry<K, V> entry : value.entrySet()) {
K k = entry.getKey();
if (k == null) {
encoder.encodeNull();
} else if (isStringKey) {
encoder.encodeKey(k.toString());
} else {
encodeMapKey(context, encoder, keyGeneric, keySerializer, k);
}
V v = entry.getValue();
if (v == null) {
encoder.encodeNull();
} else {
valSerializer.serialize(
encoder,
context,
valueGeneric, v
);
valSerializer.serialize(encoder, context, valueGeneric, v);
}
}
}
Expand All @@ -91,20 +100,30 @@ public void serialize(Encoder encoder, EncoderContext context, Argument<? extend

@Override
public void serializeInto(Encoder encoder, EncoderContext context, Argument<? extends Map<K, V>> type, Map<K, V> value) throws IOException {
Argument<K> keyGeneric = null;
Serializer<? super K> keySerializer = null;
Argument<V> valueGeneric = null;
Serializer<? super V> valSerializer = null;
for (Map.Entry<K, V> entry : value.entrySet()) {
encodeMapKey(context, encoder, entry.getKey());
K k = entry.getKey();
if (k instanceof CharSequence) {
encoder.encodeKey(k.toString());
} else {
if (keyGeneric == null || !keyGeneric.getType().equals(k.getClass())) {
keyGeneric = (Argument<K>) Argument.of(k.getClass());
keySerializer = findKeySerializer(context, keyGeneric);
}
encodeMapKey(context, encoder, keyGeneric, keySerializer, k);
}
final V v = entry.getValue();
if (v == null) {
encoder.encodeNull();
} else {
@SuppressWarnings("unchecked") final Argument<V> valueGeneric = (Argument<V>) Argument.of(v.getClass());
final Serializer<? super V> valSerializer = context.findSerializer(valueGeneric)
.createSpecific(context, valueGeneric);
valSerializer.serialize(
encoder,
context,
valueGeneric, v
);
if (valueGeneric == null || !valueGeneric.getType().equals(v.getClass())) {
valueGeneric = (Argument<V>) Argument.of(v.getClass());
valSerializer = context.findSerializer(valueGeneric).createSpecific(context, valueGeneric);
}
valSerializer.serialize(encoder, context, valueGeneric, v);
}
}
}
Expand All @@ -117,21 +136,55 @@ public boolean isEmpty(EncoderContext context, Map<K, V> value) {
}
}

private void encodeMapKey(EncoderContext context, Encoder childEncoder, K k) throws IOException {
// relies on the key type implementing toString() correctly
// perhaps we should supply conversion service
if (k instanceof CharSequence) {
childEncoder.encodeKey(k.toString());
private Serializer<K> findKeySerializer(EncoderContext context, Argument<K> keyGeneric) throws SerdeException {
try {
return (Serializer<K>) context.findSerializer(keyGeneric).createSpecific(context, keyGeneric);
} catch (SerdeException e) {
if (e.getCause() instanceof IntrospectionException) {
// The key is not introspected
return (encoder, ctx, type, value) -> convertMapKeyToStringAndEncode(ctx, encoder, value);
}
throw e;
}
}

private void encodeMapKey(EncoderContext context,
Encoder encoder,
Argument<K> keyGeneric,
Serializer<? super K> keySerializer,
K k) throws IOException {
JsonNodeEncoder keyEncoder = JsonNodeEncoder.create();
try {
keySerializer.serialize(keyEncoder, context, keyGeneric, k);
} catch (SerdeException e) {
if (e.getCause() instanceof IntrospectionException) {
// The key is not introspected
convertMapKeyToStringAndEncode(context, encoder, k);
return;
}
throw e;
}
JsonNode keyNode = keyEncoder.getCompletedValue();
if (keyNode.isString()) {
encoder.encodeKey(keyNode.getStringValue());
} else if (keyNode.isNull()) {
throw new SerdeException("Null key for a Map not allowed in JSON");
} else if (keyNode.isBoolean() || keyNode.isNumber()) {
encoder.encodeString(keyNode.coerceStringValue());
} else {
try {
final String result = context.getConversionService().convertRequired(
k,
Argument.STRING
);
childEncoder.encodeKey(result != null ? result : k.toString());
} catch (ConversionErrorException e) {
throw new SerdeException("Error converting Map key [" + k + "] to String: " + e.getMessage(), e);
convertMapKeyToStringAndEncode(context, encoder, keyNode.getValue());
}
}

private void convertMapKeyToStringAndEncode(EncoderContext context, Encoder encoder, Object keyValue) throws IOException {
try {
final String result = context.getConversionService().convertRequired(keyValue, Argument.STRING);
if (result == null) {
throw new SerdeException("Null key for a Map not allowed in JSON");
}
encoder.encodeKey(result);
} catch (ConversionErrorException ce) {
throw new SerdeException("Error converting Map key [" + keyValue + "] to String: " + ce.getMessage(), ce);
}
}

Expand Down
Loading
Loading