Skip to content

Commit

Permalink
Fix enum as map keys
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov committed Apr 22, 2024
1 parent bd82565 commit 72fd1f9
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 117 deletions.
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

0 comments on commit 72fd1f9

Please sign in to comment.