jackson-databind
jackson-databind copied to clipboard
Feature request: Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT
Is your feature request related to a problem? Please describe. Not a problem, as there is a relatively clear and simple way to achieve the same, albeit slightly more limited, functionality. See below:
Describe the solution you'd like
I would like to see the functionality of the @JsonProperty annotation on enum values be extended to output the value as integers (and potentially other types) in the JSON output (and also deserialized with the same logic). I imagine a @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) annotation on the property in the POJO would define the output format of the enum value given to @JsonProperty as a string. Hence, see the example below.
Usage example
The following example is taken from StackOverflow, an answer by user SomethingSomething:
public enum State {
@JsonProperty("0")
OFF,
@JsonProperty("1")
ON,
@JsonProperty("2")
UNKNOWN
}
The enum State in the following POJO would be serialized as {"state": "1"}, all good so far.
public record Pojo {
@JsonProperty("state") State state
}
However, consider the following case:
public record Pojo(
@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) @JsonProperty("state") State state
) {}
I would like to see this case result in JSON such as {"state": 1}, with the value being a number (int in that case) instead of a string.
Additional context
I know I can achieve the same functionality using @JsonValue and @JsonCreator annotations:
public enum State {
OFF(0),
ON(1),
UNKNOWN(2);
private final int value;
State(final int value) {
this.value = value;
}
@JsonValue
public int getValue() {
return value;
}
@JsonCreator
public State forValue(final int value) {
return Arrays.stream(values())
.filter(v -> v.value == value)
.findFirst()
.orElseThrow();
}
}
The proposed feature would allow us to shortcut these methods. Before Jackson supported the @JsonProperty annotation on enum values, the above was also the way to (de)serialize enums to other strings, this feature would extend the functionality of the @JsonProperty annotation to integers and potentially other types.
I originally posted this idea in the Ideas section of the main Jackson repo, where cowtowncoder suggested filing a feature request here.
I would like to see something like that be supported in some way or form.
so basically the combination of @JsonProperty and @JsonFormat on enum should let the property value be coerced to the type specified by the format?
Yes, @yawkat, this is would be my first idea that would use existing annotations. I could also imagine some other annotation or argument to @JsonProperty that allows arguments of type other than String.
@kistlers what version of Jackson were you using?
@yihtserns I suspect at this time we were using jackson within Spring Boot version ~ 2.7.0, so I suspect ~ jackson 2.13.3, as this version was bundled at this time.
Context
First of all, I just want to note that the best answer for https://stackoverflow.com/questions/37833557 should've been https://stackoverflow.com/a/56285491/229654, because the question author wrote:
I would like it to be 0, which is the ordinal value of OFF in the enum State.
As mentioned by the (supposed to be best) answer, to make Jackson de/serialize an enum value as its ordinal number, either annotate the enum class like this:
@JsonFormat(shape = JsonFormat.Shape.NUMBER) // or NUMBER_INT
public enum State {
OFF,
ON,
UNKNOWN
}
...or annotate the property like this:
public record Pojo(@JsonFormat(shape = JsonFormat.Shape.NUMBER) State state) { // or NUMBER_INT
}
...both will result in:
objectMapper.writeValueAsString(new Pojo(State.OFF)); // {"state":0}
objectMapper.writeValueAsString(new Pojo(State.ON)); // {"state":1}
objectMapper.writeValueAsString(new Pojo(State.UNKNOWN)); // {"state":2}
Knowing the above, I re-tested using Jackson 2.13.3, and I see the same behaviour i.e. using JsonFormat.shape=NUMBER on either the enum type or a property will result in using the enum value's ordinal number.
Is that not already the behaviour this issue wants? Unless what you want is NOT the ordinal number, but your own custom number?
Yes, @yihtserns, you are indeed correct. I want it to be any number, not just it's ordinal value. I know the functionality you suggested that would use the ordinal value. I realise now that my given example unfortunately suggests that, please check my edit to the original issue.
OK after digging through the history a bit, here's what I understand:
- Usage of
@JsonPropertyon enum value is to enable "alternative enum name" (#677). - Usage of
@JsonValueon enum property is to enable "alternative REPRESENTATION" e.g. non-ordinal number (703bf4a104193e8098f904fe33531c58c6e7f359).
For the 2nd one, the commit message said:
Make @JsonValue the canonical serialization of Enums, so that deserializer also uses it...
JsonValue's javadoc also said:
NOTE: when use for Java enums, one additional feature is that value returned by annotated method is also considered to be the value to deserialize from...
Now knowing the above, I tested this:
public enum State {
OFF(17),
ON(31),
UNKNOWN(99);
private int value;
State(int value) { this.value = value; }
@JsonValue public int value() { return this.value; }
}
...
// Serialize
mapper.writeValueAsString(new Pojo(State.OFF)); // {"state":17}
mapper.writeValueAsString(new Pojo(State.ON)); // {"state":31}
mapper.writeValueAsString(new Pojo(State.UNKNOWN)); // {"state":99}
// Deserialize
mapper.readValue("{\"state\":17}", Pojo.class); // Pojo[state=OFF]
mapper.readValue("{\"state\":31}", Pojo.class); // Pojo[state=ON]
mapper.readValue("{\"state\":99}", Pojo.class); // Pojo[state=UNKNOWN]
// Try to use ordinal number
mapper.readValue("{\"state\":0}", Pojo.class); // InvalidFormatException...not one of the values accepted for Enum class: [99, 17, 31]
Seems like a JsonCreator factory method is not necessary at all, which I believe is the main motivation for opening this issue?
(I did find some complaints about needing JsonCreator factory method to make things work (#1850), but it was a (fixed) bug.)
Oh, this is new to me. I suppose I never fully read the JavaDoc of @JsonValue to find this behaviour (Probably because I simply would not have expected it). Thanks for digging it up!
I think this does exactly what I wanted this Issue to achieve since the @JsonCreator method in the first example exactly reverses the mapping of the @JsonValue method.
I just tested this and it then also again plays nicely with @JsonFormat(shape = JsonFormat.Shape.STRING) or similar:
record PojoString(@JsonFormat(shape = JsonFormat.Shape.STRING) State state) {}
// Serialize as string
mapper.writeValueAsString(new PojoString(State.OFF)); // {"state":"17"}
mapper.writeValueAsString(new PojoString(State.ON)); // {"state":"31"}
mapper.writeValueAsString(new PojoString(State.UNKNOWN)); // {"state":"99"}
// Deserialize as string
mapper.readValue("{\"state\":\"17\"}", PojoString.class); // PojoString[state=OFF]
mapper.readValue("{\"state\":\"31\"}", PojoString.class); // PojoString[state=ON]
mapper.readValue("{\"state\":\"99\"}", PojoString.class); // PojoString[state=UNKNOWN]
as one would expect @JsonFormat to work anyway.
And I also realized at some point in the last 8 months that the getter is also not required, one can also just annotate the field directly and all the tests still pass:
public enum State {
OFF(17),
ON(31),
UNKNOWN(99);
@JsonValue private int value;
State(int value) { this.value = value; }
}
My original idea would have also removed the need for the private field and the @JsonValue annotation. But not requiring the @JsonCreator method is already a large improvement in my opinion.
Yeah, handling of @JsonValue wrt Enum has been improved over time; Javadoc comments may be quite new as well.
Enums are tricky, especially wrt ordinals (and esp. if considering "stringified numbers").
But hopefully things work out better now.
As far as I understand it now, using @JsonProperty can be considered a shortcut notation compared to using @JsonValue with a String field on the enum. In that case, I would expect the following enums to (de)serialize in the same way, when applying @JsonFormat(shape = JsonFormat.Shape.NUMBER) to the field:
enum StateField {
OFF("17"),
ON("31"),
UNKNOWN("99");
@JsonValue private final String value;
StateField(String value) {
this.value = value;
}
}
enum StateProperty {
@JsonProperty("17") OFF,
@JsonProperty("31") ON,
@JsonProperty("99") UNKNOWN
}
record Pojo(
@JsonFormat(shape = JsonFormat.Shape.NUMBER) StateField stateField,
@JsonFormat(shape = JsonFormat.Shape.NUMBER) StateProperty stateProperty) {}
However, they do not:
// Serialize
mapper.writeValueAsString(new Pojo(StateField.OFF, StateProperty.OFF)); // {"stateField":"17", "stateProperty":0}
mapper.writeValueAsString(new Pojo(StateField.ON, StateProperty.ON)); // {"stateField":"31", "stateProperty":1}
mapper.writeValueAsString(new Pojo(StateField.UNKNOWN, StateProperty.UNKNOWN)); // {"stateField":"99", "stateProperty":2}
// Deserialize
mapper.readValue("{\"stateField\":\"17\",\"stateProperty\":0}", Pojo.class); // Pojo[stateField=OFF,stateProperty=UNKNOWN]
mapper.readValue("{\"stateField\":\"31\",\"stateProperty\":1}", Pojo.class); // Pojo[stateField=ON,stateProperty=UNKNOWN]
mapper.readValue("{\"stateField\":\"99\",\"stateProperty\":2}", Pojo.class); // Pojo[stateField=UNKNOWN,stateProperty=UNKNOWN]
On the first enum, the annotation is ignored, as Jackson does not coerce the String value to an int. But in the second enum, the ordinal value is used. I suppose this is expected behaviour, yet somewhat surprising to me.
Coercions/conversions between Numbers and Strings are tricky... and in a way I wish there was no default support for (de)serializing Enums by index (without some sort of explicit configuration). So existing behavior is defined in many cases as "that's how it is implemented" instead of proper definitions.
But I will note one thing: @JsonValue behavior is not quite like that of @JsonProperty -- with POJOs they are very distinct. @JsonValue "replaces" serialization completely. That doesn't really explain difference you are seeing, but I mention this as background. It would play a big role of @JsonValue annotated field or getter had type of int tho, in which case those values should be used instead of Enum index.
Conversely intent with @JsonProperty is to give an alternate String, so it cannot really be used to specify number (stringified or otherwise) for Enum -- internally number for Enums used would still be index.
But I can see why from user perspective this does not make much difference, and passing Stringified number (because @JsonProperty can not take int value) is the thing to use, along with shape.
I just don't know if:
- It is possible to define consistent set of rules to indicate desired way everything should work together, and
- ... to implement (1)
since everything is sort of cobbled together from separate pieces of functionality.
Thanks for the detailed answer, I appreciate it. I expected something along those lines to explain the observed behaviour.
I suppose we can close this issue then?