Skip to content

Commit

Permalink
Improve Enum serialization via AnnotatedClass (#3991)
Browse files Browse the repository at this point in the history
  • Loading branch information
JooHyukKim authored Jun 18, 2023
1 parent 4cb1712 commit 66e4ebd
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
JsonFormat.Feature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)).orElse(_useDefaultValueForUnknownEnum);
Boolean useNullForUnknownEnum = Optional.ofNullable(findFormatFeature(ctxt, property, handledType(),
JsonFormat.Feature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)).orElse(_useNullForUnknownEnum);

return withResolved(caseInsensitive, useDefaultValueForUnknownEnum, useNullForUnknownEnum);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public static EnumSerializer construct(Class<?> enumClass, SerializationConfig c
* between name() and toString(), need to construct `EnumValues` with names,
* handle toString() case dynamically (for example)
*/
EnumValues v = EnumValues.constructFromName(config, (Class<Enum<?>>) enumClass);
EnumValues v = EnumValues.constructFromName(config, beanDesc.getClassInfo());
EnumValues valuesByEnumNaming = constructEnumNamingStrategyValues(config, (Class<Enum<?>>) enumClass, beanDesc.getClassInfo());
Boolean serializeAsIndex = _isShapeWrittenUsingIndex(enumClass, format, true, null);
return new EnumSerializer(v, serializeAsIndex, valuesByEnumNaming);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public static JsonSerializer<Object> getFallbackKeySerializer(SerializationConfi
// for subtypes.
if (ClassUtil.isEnumType(rawKeyType)) {
return EnumKeySerializer.construct(rawKeyType,
EnumValues.constructFromName(config, (Class<Enum<?>>) rawKeyType),
EnumValues.constructFromName(config, annotatedClass),
EnumSerializer.constructEnumNamingStrategyValues(config, (Class<Enum<?>>) rawKeyType, annotatedClass));
}
}
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fasterxml.jackson.databind.util;

import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import java.util.*;

import com.fasterxml.jackson.core.SerializableString;
Expand Down Expand Up @@ -41,6 +42,9 @@ public static EnumValues construct(SerializationConfig config, Class<Enum<?>> en
return constructFromName(config, enumClass);
}

/**
* @deprecated Since 2.16, use {@link #constructFromName(MapperConfig, AnnotatedClass)} instead.
*/
public static EnumValues constructFromName(MapperConfig<?> config, Class<Enum<?>> enumClass)
{
// Enum types with per-instance sub-classes need special handling
Expand All @@ -65,6 +69,39 @@ public static EnumValues constructFromName(MapperConfig<?> config, Class<Enum<?>
return construct(enumClass, textual);
}


/**
* @since 2.16
*/
public static EnumValues constructFromName(MapperConfig<?> config, AnnotatedClass annotatedClass)
{
// prepare data
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
final boolean useLowerCase = config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls0);

// introspect
String[] names = ai.findEnumValues(config, annotatedClass,
enumConstants, new String[enumConstants.length]);

// build
SerializableString[] textual = new SerializableString[enumConstants.length];
for (int i = 0, len = enumConstants.length; i < len; ++i) {
Enum<?> enumValue = enumConstants[i];
String name = names[i];
if (name == null) {
name = enumValue.name();
}
if (useLowerCase) {
name = name.toLowerCase();
}
textual[enumValue.ordinal()] = config.compileString(name);
}
return construct(enumCls, textual);
}

public static EnumValues constructFromToString(MapperConfig<?> config, Class<Enum<?>> enumClass)
{
Class<? extends Enum<?>> cls = ClassUtil.findEnumType(enumClass);
Expand Down Expand Up @@ -121,6 +158,34 @@ public static EnumValues construct(Class<Enum<?>> enumClass,
return new EnumValues(enumClass, externalValues);
}

/*
/**********************************************************************
/* Internal Helpers
/**********************************************************************
*/

protected static Class<Enum<?>> _enumClass(Class<?> enumCls0) {
return (Class<Enum<?>>) enumCls0;
}

/**
* Helper method <b>slightly</b> different from {@link EnumResolver#_enumConstants(Class)},
* with same method name to keep calling methods more consistent.
*/
protected static Enum<?>[] _enumConstants(Class<?> enumCls) {
final Enum<?>[] enumValues = ClassUtil.findEnumType(enumCls).getEnumConstants();
if (enumValues == null) {
throw new IllegalArgumentException("No enum constants for class "+enumCls.getName());
}
return enumValues;
}

/*
/**********************************************************************
/* Public API
/**********************************************************************
*/

public SerializableString serializedValueFor(Enum<?> key) {
return _textual[key.ordinal()];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,30 @@ public void testEnumDeserSuccess() throws Exception {
Enum2787 result = mapper.readValue(q("B_MIXIN_PROP"), Enum2787.class);

assertEquals(Enum2787.ITEM_B, result);

String value = mapper.writeValueAsString(EnumMixin2787.ITEM_B);
}

public void testEnumMixinRoundTripSerDeser() throws Exception {
// ser -> deser
ObjectMapper mapper = MAPPER.addMixIn(Enum2787.class, EnumMixin2787.class);
// from
String result = mapper.writeValueAsString(Enum2787.ITEM_B);
assertEquals(q("B_MIXIN_PROP"), result);
// to
Enum2787 result2 = mapper.readValue(result, Enum2787.class);
assertEquals(Enum2787.ITEM_B, result2);
}

public void testEnumMixinRoundTripDeserSer() throws Exception {
// deser -> ser
ObjectMapper mapper = MAPPER.addMixIn(Enum2787.class, EnumMixin2787.class);
// from
Enum2787 result = mapper.readValue(q("B_MIXIN_PROP"), Enum2787.class);
assertEquals(Enum2787.ITEM_B, result);
// to
String value = mapper.writeValueAsString(result);
assertEquals(q("B_MIXIN_PROP"), value);
}

public void testBeanMixin() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fasterxml.jackson.databind.deser.enums;

import com.fasterxml.jackson.databind.module.TestTypeModifierNameResolution;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -93,6 +94,34 @@ public String toString() {
;
}

// [databind#677]
static enum EnumWithPropertyAnnoBase {
@JsonProperty("a")
A,
// For this value, force use of anonymous sub-class, to ensure things still work
@JsonProperty("b")
B {
@Override
public String toString() {
return "bb";
}
}
}

// [databind#677]
static enum EnumWithPropertyAnnoMixin {
@JsonProperty("a_mixin")
A,
// For this value, force use of anonymous sub-class, to ensure things still work
@JsonProperty("b_mixin")
B {
@Override
public String toString() {
return "bb";
}
}
}

// [databind#1161]
enum Enum1161 {
A, B, C;
Expand Down Expand Up @@ -512,6 +541,27 @@ public void testEnumWithJsonPropertyRename() throws Exception
assertSame(EnumWithPropertyAnno.A, result[1]);
}

/**
* {@link #testEnumWithJsonPropertyRename()}
*/
public void testEnumWithJsonPropertyRenameMixin() throws Exception
{
ObjectMapper mixinMapper = jsonMapperBuilder()
.addMixIn(EnumWithPropertyAnnoBase.class, EnumWithPropertyAnnoMixin.class)
.build();
String json = mixinMapper.writeValueAsString(new EnumWithPropertyAnnoBase[] {
EnumWithPropertyAnnoBase.B, EnumWithPropertyAnnoBase.A
});
assertEquals("[\"b_mixin\",\"a_mixin\"]", json);

// and while not really proper place, let's also verify deser while we're at it
EnumWithPropertyAnnoBase[] result = mixinMapper.readValue(json, EnumWithPropertyAnnoBase[].class);
assertNotNull(result);
assertEquals(2, result.length);
assertSame(EnumWithPropertyAnnoBase.B, result[0]);
assertSame(EnumWithPropertyAnnoBase.A, result[1]);
}

// [databind#1161], unable to switch READ_ENUMS_USING_TO_STRING
public void testDeserWithToString1161() throws Exception
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,42 @@ public void foo() { }
public String toString() { return name() + " as string"; }
}

// [databind#2457]
enum MyEnum2457Base {
@JsonProperty("a_base")
A,
@JsonProperty("b_base")
B() {
// just to ensure subclass construction
@Override
public void foo() { }
};

// needed to force subclassing
public void foo() { }

@Override
public String toString() { return name() + " as string"; }
}

// [databind#2457]
enum MyEnum2457Mixin {
@JsonProperty("a_mixin")
A,
@JsonProperty("b_mixin")
B() {
// just to ensure subclass construction
@Override
public void foo() { }
};

// needed to force subclassing
public void foo() { }

@Override
public String toString() { return name() + " as string"; }
}

/*
/**********************************************************
/* Test methods, basic
Expand Down Expand Up @@ -261,6 +297,28 @@ public void testCustomEnumAsRootMapKey() throws Exception
.writeValueAsString(map));
}

/**
* @see #testCustomEnumAsRootMapKey
*/
// [databind#2457]
public void testCustomEnumAsRootMapKeyMixin() throws Exception
{
ObjectMapper mixinMapper = JsonMapper.builder()
.addMixIn(MyEnum2457Base.class, MyEnum2457Mixin.class)
.build();
final Map<MyEnum2457Base, String> map = new LinkedHashMap<>();
map.put(MyEnum2457Base.A, "1");
map.put(MyEnum2457Base.B, "2");
assertEquals(a2q("{'a_mixin':'1','b_mixin':'2'}"),
mixinMapper.writeValueAsString(map));

// But should be able to override
assertEquals(a2q("{'"+MyEnum2457Base.A.toString()+"':'1','"+MyEnum2457Base.B.toString()+"':'2'}"),
mixinMapper.writer()
.with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)
.writeValueAsString(map));
}

/*
/**********************************************************************
/* Test methods: case-insensitive Enums
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.fasterxml.jackson.databind.ser.enums;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.ObjectMapper;

public class EnumSerializationMixinTest extends BaseMapTest
{

static enum EnumBaseA {
ITEM_A {
@Override
public String toString() {
return "A_base";
}
},

@JsonAlias({"B_ORIGIN_ALIAS_1", "B_ORIGIN_ALIAS_2"})
@JsonProperty("B_ORIGIN_PROP")
ITEM_B,

@JsonAlias({"C_ORIGIN_ALIAS"})
@JsonProperty("C_COMMON")
ITEM_C_BASE,

ITEM_ORIGIN
}

static enum EnumMixinA {
ITEM_A {
@Override
public String toString() {
return "A_mixin";
}
},

@JsonProperty("B_MIXIN_PROP")
ITEM_B,

@JsonAlias({"C_MIXIN_ALIAS_1", "C_MIXIN_ALIAS_2"})
@JsonProperty("C_COMMON")
ITEM_C_MIXIN,

ITEM_MIXIN;

@Override
public String toString() {
return "SHOULD NOT USE WITH TO STRING";
}
}

public void testSerialization() throws Exception {
ObjectMapper mixinMapper = jsonMapperBuilder()
.addMixIn(EnumBaseA.class, EnumMixinA.class).build();

// equal name(), different toString() value
assertEquals(q("ITEM_A"), _w(EnumBaseA.ITEM_A, mixinMapper));

// equal name(), differnt @JsonProperty
assertEquals(q("B_MIXIN_PROP"), _w(EnumBaseA.ITEM_B, mixinMapper));

// different name(), equal @JsonProperty
assertEquals(q("C_COMMON"), _w(EnumBaseA.ITEM_C_BASE, mixinMapper));

// different name(), equal ordinal()
assertEquals(q("ITEM_ORIGIN"), _w(EnumBaseA.ITEM_ORIGIN, mixinMapper));
}

/**
* Helper method to {@link ObjectMapper#writeValueAsString(Object)}
*/
private <T> String _w(T value, ObjectMapper mapper) throws Exception {
return mapper.writeValueAsString(value);
}
}

0 comments on commit 66e4ebd

Please sign in to comment.