Jackson枚举序列化和反序列化

288

我正在使用JAVA 1.6和Jackson 1.9.9,我有一个枚举

public enum Event {
    FORGOT_PASSWORD("forgot password");

    private final String value;

    private Event(final String description) {
        this.value = description;
    }

    @JsonValue
    final String value() {
        return this.value;
    }
}

我已经添加了@JsonValue,它似乎可以把对象序列化为:

{"event":"forgot password"}

但是当我尝试反序列化时,我得到了一个

Caused by: org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.globalrelay.gas.appsjson.authportal.Event from String value 'forgot password': value not one of declared Enum instance names

我这里缺少什么?


4
你尝试过 {"Event":"FORGOT_PASSWORD"} 吗?注意 Event 和 FORGOT_PASSWORD 都是大写。 - OldCurmudgeon
来到这里的人也要检查getter setter语法,如果你遵循不同的命名约定,例如,而不是getValue,这个GetValue是不起作用的。 - Davut Gürbüz
19个回答

341

如果您希望完全将枚举类与其JSON表示分离,那么由@xbakesx指出的序列化程序/反序列化程序解决方案是一个很好的选择。

或者,如果您更喜欢一个自包含的解决方案,则基于@JsonCreator@JsonValue注释的实现更加方便。所以,借鉴@Stanley提供的示例,以下是一个完整的自包含解决方案(Java 6,Jackson 1.9):

public enum DeviceScheduleFormat {

    Weekday,
    EvenOdd,
    Interval;

    private static Map<String, DeviceScheduleFormat> namesMap = new HashMap<String, DeviceScheduleFormat>(3);

    static {
        namesMap.put("weekday", Weekday);
        namesMap.put("even-odd", EvenOdd);
        namesMap.put("interval", Interval);
    }

    @JsonCreator
    public static DeviceScheduleFormat forValue(String value) {
        return namesMap.get(StringUtils.lowerCase(value));
    }

    @JsonValue
    public String toValue() {
        for (Entry<String, DeviceScheduleFormat> entry : namesMap.entrySet()) {
            if (entry.getValue() == this)
                return entry.getKey();
        }

        return null; // or fail
    }
}

47
对一些人来说可能很明显,但请注意@JsonValue用于序列化,@JsonCreator用于反序列化。如果您不同时进行这两个操作,则只需要其中一个。 - acvcu
12
我非常不喜欢这种解决方案,因为它引入了两个真值来源。开发者必须始终记得在两个位置添加名称。我更喜欢一种解决方案,它可以在不用将枚举的内部装饰成映射的情况下,直接做正确的事情。 - mttdbrd
2
@mttdbrd,您可以通过在构造函数期间将对象添加到地图中来避免这种情况。 - Langley
5
@ttdbrd 这个用于统一真相的方案怎么样? https://gist.github.com/Scuilion/036c53fd7fee2de89701a95822c0fb60 - KevinO
6
您可以使用 YourEnum.values() 替代静态地图,它会返回一个 YourEnum 数组,并对其进行迭代。 - Valeriy K.
显示剩余7条评论

279

请注意,从2015年6月的此提交开始(Jackson 2.6.2及以上版本),您现在可以简单地编写:

public enum Event {
    @JsonProperty("forgot password")
    FORGOT_PASSWORD;
}

这里记录了此行为的文档:https://fasterxml.github.io/jackson-annotations/javadoc/2.11/com/fasterxml/jackson/annotation/JsonProperty.html

从Jackson 2.6开始,此注释也可用于更改枚举的序列化方式:

 public enum MyEnum {
      @JsonProperty("theFirstValue") THE_FIRST_VALUE,
      @JsonProperty("another_value") ANOTHER_VALUE;
 }

作为使用JsonValue注释的替代方案。


1
不幸的是,当将枚举转换为字符串时,这不会返回属性。 - Nicholas
2
此解决方案适用于枚举类型的序列化和反序列化。已在2.8版本中进行了测试。 - Downhillski
10
看起来这个属性并没有被弃用:https://github.com/FasterXML/jackson-annotations/blob/master/src/main/java/com/fasterxml/jackson/annotation/JsonProperty.java - Pablo Fradua
2
这对我没有任何作用,使用的是Jackson 2.9.10。 - David M. Karr
3
我在(2.11)文档中添加了一个官方链接,明确说明@JsonProperty可以在2.6及以后版本中像这样使用。 - Per Lundberg
显示剩余3条评论

101

你应该创建一个静态工厂方法,该方法接受单个参数,并使用@JsonCreator进行注释(自Jackson 1.2起可用)

@JsonCreator
public static Event forValue(String value) { ... }

点击这里了解有关JsonCreator注释的更多信息。


11
这是最干净、最简洁的解决方案,其余的只是大量的样板代码,应该尽一切可能避免使用! - Clint Eastwood
6
使用@JSONValue进行序列化,使用@JSONCreator进行反序列化。 - Chiranjib
@JsonCreator public static Event valueOf(int intValue) { ... } 用于将 int 反序列化为 Event 枚举。 - Eido95
2
@ClintEastwood,其他解决方案是否应该避免取决于您是否想将序列化/反序列化问题与枚举分开。 - Asa

49

实际答案:

枚举默认的反序列化程序使用 .name() 来反序列化,因此它没有使用 @JsonValue。所以正如@OldCurmudgeon指出的那样,您需要传入 {"event": "FORGOT_PASSWORD"} 以匹配 .name() 值。

另一个选项(假设您希望写入和读取的JSON值相同)...

更多信息:

Jackson 还有一种管理序列化和反序列化过程的方法。您可以指定这些注释来使用自己的自定义序列化程序和反序列化程序:

@JsonSerialize(using = MySerializer.class)
@JsonDeserialize(using = MyDeserializer.class)
public final class MyClass {
    ...
}

那么你需要编写类似下面这样的 MySerializerMyDeserializer

MySerializer

public final class MySerializer extends JsonSerializer<MyClass>
{
    @Override
    public void serialize(final MyClass yourClassHere, final JsonGenerator gen, final SerializerProvider serializer) throws IOException, JsonProcessingException
    {
        // here you'd write data to the stream with gen.write...() methods
    }

}

MyDeserializer

public final class MyDeserializer extends org.codehaus.jackson.map.JsonDeserializer<MyClass>
{
    @Override
    public MyClass deserialize(final JsonParser parser, final DeserializationContext context) throws IOException, JsonProcessingException
    {
        // then you'd do something like parser.getInt() or whatever to pull data off the parser
        return null;
    }

}

最后一点,特别是对于将此操作应用于序列化方法为getYourValue()的枚举JsonEnum的情况,您的序列化程序和反序列化程序可能如下所示:

public void serialize(final JsonEnum enumValue, final JsonGenerator gen, final SerializerProvider serializer) throws IOException, JsonProcessingException
{
    gen.writeString(enumValue.getYourValue());
}

public JsonEnum deserialize(final JsonParser parser, final DeserializationContext context) throws IOException, JsonProcessingException
{
    final String jsonValue = parser.getText();
    for (final JsonEnum enumValue : JsonEnum.values())
    {
        if (enumValue.getYourValue().equals(jsonValue))
        {
            return enumValue;
        }
    }
    return null;
}

3
使用自定义(反)序列化器会破坏简单性(顺便说一句,使用Jackson是值得的),因此只有在真正复杂的情况下才需要。使用如下所述的@JsonCreator,并检查此注释。 - Dmitry Gryazin
2
这个解决方案最适合OP问题中引入的有点疯狂的问题。真正的问题在于OP想要以渲染形式返回结构化数据。也就是说,他们正在返回已经包含用户友好字符串的数据。但是为了将渲染形式转换回标识符,我们需要一些可以反转换的代码。hacky的接受答案想要使用映射来处理转换,但需要更多的维护。通过这个解决方案,您可以添加新的枚举类型,然后您的开发人员就可以继续他们的工作了。 - mttdbrd

44

我找到了一个非常好而且简洁的解决方案,特别是在你无法修改枚举类的情况下非常有用。这时,您应该提供一个启用了特定功能的自定义ObjectMapper。 这些特性自Jackson 1.6以来就可用。所以您只需在枚举中编写toString()方法。

public class CustomObjectMapper extends ObjectMapper {
    @PostConstruct
    public void customConfiguration() {
        // Uses Enum.toString() for serialization of an Enum
        this.enable(WRITE_ENUMS_USING_TO_STRING);
        // Uses Enum.toString() for deserialization of an Enum
        this.enable(READ_ENUMS_USING_TO_STRING);
    }
}

还有更多与枚举相关的功能可用,详见此处:

https://github.com/FasterXML/jackson-databind/wiki/Serialization-Features https://github.com/FasterXML/jackson-databind/wiki/Deserialization-Features


13
不确定为什么需要扩展该类,您可以在ObjectMapper的实例上启用此功能。 - mttdbrd
+1 因为他向我指出了可以在Spring应用程序.yml中使用的[READ|WRITE]_ENUMS_USING_TO_STRING。 - HelLViS69
1
谢谢,您的答案帮助我解决了 Retrofit 的问题。如果您想在序列化中使用序数,则可以使用 SerializationFeature.WRITE_ENUMS_USING_INDEX。 - user1154390
谢谢你提供的配置建议,它帮助我解决了我的问题。 - Arnas Ivanavičius
这对我很有帮助,因为POJO是外部的,没有办法更新它们。 - NullPointerException

14

我喜欢这个被采纳的答案,不过我想稍微改进一下(考虑到现在有高于Java 6版本的可用版本)。

例子:

    public enum Operation {
        EQUAL("eq"),
        NOT_EQUAL("ne"),
        LESS_THAN("lt"),
        GREATER_THAN("gt");

        private final String value;

        Operation(String value) {
            this.value = value;
        }

        @JsonValue
        public String getValue() {
            return value;
        }

        @JsonCreator
        public static Operation forValue(String value) {
            return Arrays.stream(Operation.values())
                .filter(op -> op.getValue().equals(value))
                .findFirst()
                .orElseThrow(); // depending on requirements: can be .orElse(null);
        }
    }

13

尝试一下。

public enum Event {

    FORGOT_PASSWORD("forgot password");

    private final String value;

    private Event(final String description) {
        this.value = description;
    }

    private Event() {
        this.value = this.name();
    }

    @JsonValue
    final String value() {
        return this.value;
    }
}

7

您可以自定义任何属性的反序列化。

通过注解JsonDeserialize (import com.fasterxml.jackson.databind.annotation.JsonDeserialize) 声明您的反序列化类,用于处理该属性。如果这是一个枚举类型:

@JsonDeserialize(using = MyEnumDeserialize.class)
private MyEnum myEnum;

这样,您的类将用于反序列化属性。以下是完整示例:

public class MyEnumDeserialize extends JsonDeserializer<MyEnum> {

    @Override
    public MyEnum deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        MyEnum type = null;
        try{
            if(node.get("attr") != null){
                type = MyEnum.get(Long.parseLong(node.get("attr").asText()));
                if (type != null) {
                    return type;
                }
            }
        }catch(Exception e){
            type = null;
        }
        return type;
    }
}

纳撒尼尔·福特,好点了吗? - Fernando Nogueira
1
是的,这是一个更好的答案;它提供了一些上下文。不过,我会更进一步地讨论为什么以这种方式添加反序列化可以解决OP的具体障碍。 - Nathaniel Ford

5

有多种方法可以实现将JSON对象反序列化为枚举类型。我最喜欢的方法是创建一个内部类:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;

import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.fasterxml.jackson.annotation.JsonFormat.Shape.OBJECT;

@JsonFormat(shape = OBJECT)
public enum FinancialAccountSubAccountType {
  MAIN("Main"),
  MAIN_DISCOUNT("Main Discount");

  private final static Map<String, FinancialAccountSubAccountType> ENUM_NAME_MAP;
  static {
    ENUM_NAME_MAP = Arrays.stream(FinancialAccountSubAccountType.values())
      .collect(Collectors.toMap(
        Enum::name,
        Function.identity()));
  }

  private final String displayName;

  FinancialAccountSubAccountType(String displayName) {
    this.displayName = displayName;
  }

  @JsonCreator
  public static FinancialAccountSubAccountType fromJson(Request request) {
    return ENUM_NAME_MAP.get(request.getCode());
  }

  @JsonProperty("name")
  public String getDisplayName() {
    return displayName;
  }

  private static class Request {
    @NotEmpty(message = "Financial account sub-account type code is required")
    private final String code;
    private final String displayName;

    @JsonCreator
    private Request(@JsonProperty("code") String code,
                    @JsonProperty("name") String displayName) {
      this.code = code;
      this.displayName = displayName;
    }

    public String getCode() {
      return code;
    }

    @JsonProperty("name")
    public String getDisplayName() {
      return displayName;
    }
  }
}

5

这是另一个使用字符串值而不是映射的示例。

public enum Operator {
    EQUAL(new String[]{"=","==","==="}),
    NOT_EQUAL(new String[]{"!=","<>"}),
    LESS_THAN(new String[]{"<"}),
    LESS_THAN_EQUAL(new String[]{"<="}),
    GREATER_THAN(new String[]{">"}),
    GREATER_THAN_EQUAL(new String[]{">="}),
    EXISTS(new String[]{"not null", "exists"}),
    NOT_EXISTS(new String[]{"is null", "not exists"}),
    MATCH(new String[]{"match"});

    private String[] value;

    Operator(String[] value) {
        this.value = value;
    }

    @JsonValue
    public String toStringOperator(){
        return value[0];
    }

    @JsonCreator
    public static Operator fromStringOperator(String stringOperator) {
        if(stringOperator != null) {
            for(Operator operator : Operator.values()) {
                for(String operatorString : operator.value) {
                    if (stringOperator.equalsIgnoreCase(operatorString)) {
                        return operator;
                    }
                }
            }
        }
        return null;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接