I want to use Jackson JSON to serialize/deserialize a class containing an enum object. My class is:
class Bar {
#JsonProperty("rateType")
#JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
private ReturnedRateType rateType;
public ReturnedRateType getRateType() {
return rateType;
}
public void setRateType(ReturnedRateType rateType) {
this.rateType = rateType;
}
}
The enum class ReturnedRateType is defined as:
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ReturnedRateType {
AA("AA"),
BB("BB"),
CC("CC");
#JsonProperty("value")
private String value;
ReturnedRateType(String value) {
this.value = value;
}
#JsonCreator
public static ReturnedRateType fromValue(final String value) {
if (value != null) {
for (ReturnedRateType type : ReturnedRateType.values()) {
if (value.equalsIgnoreCase(type.value)) {
return type;
}
}
}
return null;
}
}
As you see, I added #JsonFormat annotation to tell Jackson to serialize this enum as POJO, and added #JsonCreator annotation to get a static factory method from given string to enum object. Since Jackson can only serialize but can't deserialize from object representation to enum, I added the following customized deserializer for the enum ReturnedRateType:
public class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {
#Override
public ReturnedRateType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString());
if(type != null)
return type;
throw new JsonMappingException("invalid value for ReturnedRateType");
}
}
But when I tested deserialization from a JSON string to enum, I got the error. The JSON string is:
{"rateType": {"value": "AA"}}
My test code is:
#Test
public void RateTypeToEnum() {
String json = "{\"rateType\": {\"value\": \"AA\"}}";
System.out.println(json);
ObjectMapper mapper = new ObjectMapper();
Bar bar = null;
try {
bar = mapper.readValue(json, Bar.class);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(bar.getRateType());
}
I expect to see the output should be AA. But jp.getValueAsString() in my customized deserializer ReturnedRateTypeDeserializer is null during the execution:
ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString()); //jp.getValueAsString() is null here!
Thus it returns error. So what is wrong here?
According to the Jackson 2.5.X documentation on the JsonFormat annotation the Shape.Object does not work for the enum deserialisation:
Enums: Shapes JsonFormat.Shape.STRING and JsonFormat.Shape.NUMBER can
be used to change between numeric (index) and textual (name or
toString()); but it is also possible to use JsonFormat.Shape.OBJECT
to serialize (but not deserialize).
I'd make the JsonCreator static method accept a JsonNode and read the string value from it.
Note that this would work since 2.5.X. In early versions you would need to write a custom deserialiser. Here is an example:
public class JacksonEnumObjectShape {
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
#JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
public enum ReturnedRateType {
AA("AA"),
BB("BB"),
CC("CC");
#JsonProperty("value")
private String value;
ReturnedRateType(String value) {
this.value = value;
}
#JsonCreator
public static ReturnedRateType fromValue(final JsonNode jsonNode) {
for (ReturnedRateType type : ReturnedRateType.values()) {
if (type.value.equals(jsonNode.get("value").asText())) {
return type;
}
}
return null;
}
}
// can be avoided since 2.5
public static class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {
#Override
public ReturnedRateType deserialize(
final JsonParser jp,
final DeserializationContext ctxt) throws IOException {
final JsonNode jsonNode = jp.readValueAsTree();
return ReturnedRateType.fromValue(jsonNode);
}
}
public static void main(String[] args) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
final String json = mapper.writeValueAsString(ReturnedRateType.AA);
System.out.println(json);
System.out.println(mapper.readValue(json, ReturnedRateType.class));
}
}
Output:
{"value":"AA"}
AA
Related
I have a simple class as property of mage:
// getter/setter omitted for brevity
public class Magic() {
String Spell;
int strength;
}
public class Mage() {
String name;
Magic magic;
}
I need to deserialize JSON from 2 different source strings:
{
"name" : "Sauron",
"magic" : {
"spell" : "Tamador",
"strenght" : 10
}
}
and
{
"name" : "Gandalf",
"magic" : "You shall not pass"
}
or even "You shall not pass" -> Magic object
I thought going with #JsonDeserialize(using = MagicDeserializer.class) would be the way to go with Jackson, but the Parser barfs with "Unrecognized token". Is there a way I can intercept the loading to do my own parsing?
The idea of a custom deserializer is correct, you can extends the StdDeserializer class and in its deserialize method convert the json to a JsonNode separating the two Stringand Object distinct values associated to the magic key in the json:
public class MagicDeserializer extends StdDeserializer<Magic> {
public MagicDeserializer() {
super(Magic.class);
}
#Override
public Magic deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
final ObjectCodec codec = jp.getCodec();
JsonNode root = codec.readTree(jp);
Magic magic = new Magic();
if (root.isTextual()) { //<- magic is a string
magic.setSpell(root.textValue());
return magic;
}
//ok, so magic is an Magic object
return codec.treeToValue(root, Magic.class);
}
}
Then if you annotate your Magic field you can deserialize both the jsons:
#Data
public class Mage {
private String name;
#JsonDeserialize(using = MagicDeserializer.class)
private Magic magic;
}
#Data
public class Magic {
private String Spell;
private int strength;
}
Mage sauron = mapper.readValue(json1, Mage.class);
System.out.println(mapper.writeValueAsString(sauron));
Mage gandalf = mapper.readValue(json2, Mage.class);
System.out.println(mapper.writeValueAsString(gandalf));
I want to do something like this with subtypes.
I have 3 different types of objects:
{
"value": "string"
}
{
"value": {
"type": "obj1"
}
}
{
"value": {
"type": "obj2"
}
}
value can either be a string or an object.
The corresponding Java classes are
public interface Value {
}
public class ValueString implements Value {
String value;
}
public abstract class ValueObj implements Value{
public String type;
}
public class ValueObj1 extends ValueObj {
private Obj1 value;
}
public class ValueObj2 extends ValueObj {
private Obj2 value;
}
I don't mind having a discriminator inside Obj1 and Obj2, but there is no place for one when the value is just a string. Is there a way that I can set this up so that if the value is a string, it deserializes to ValueString, but if it is an object, it deserializes to the correct ValueObj1 or ValueObj2?
It can be easily done by creating a custom deserializer first:
p.s. I assumed that there're only three types of objects as you posted.
public class ValueDeserializer extends StdDeserializer<Value> {
public ValueDeserializer() {
this(null);
}
protected ValueDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Value deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
if (jsonNode.get("value").isValueNode()) {
return new ValueString(jsonNode.get("value").asText());
} else if ("obj1".equals(jsonNode.get("value").get("type").asText())) {
ValueObj1 valueObj1 = new ValueObj1();
// The logic to handle type obj1
return valueObj1;
} else {
ValueObj2 valueObj2 = new ValueObj2();
// The logic to handle type obj2
return valueObj2;
}
}
Then simply annotate class Value with #JsonDeserialize as follows:
#JsonDeserialize(using = ValueDeserializer.class)
public interface Value {
}
Finally, let Jackson do the rest for you:
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(objectMapper.readValue(jsonStr, Value.class)));
Is there a way to deserialize an enum which works for both the name and the object notation. I do want to keep the Shape as object for the deserialization though
e.g. This works for "type": {"name":"MYENUM"}, but what would I need to add to have it also work for "type": "MYENUM"
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum MyType {
#JsonProperty("MYENUM")
MYENUM("MyEnum")
public final String name = name();
MyType(String value) {
this.value = value;
}
#JsonCreator
public static MyType deserialize (#JsonProperty("name") String name) {
return MyType.valueOf(name);
}
}
Have tried adding a delegate like this
#JsonCreator(mode=JsonCreator.Mode.DELEGATING)
public static MyType deserializeString (String name) {
return MyType.valueOf(name);
}
One way to solve your problem is a custom deserializer extending StdDeserializer and inside of it check if your json file is in the form of "type": {"name":"MYENUM"} or {"type": "MYENUM"}. This checking can be obtained with the JsonNode#isObject method :
public class MyTypeDeserializer extends StdDeserializer<MyType> {
public MyTypeDeserializer() {
this(null);
}
public MyTypeDeserializer(Class<?> vc) {
super(vc);
}
#Override
public MyType deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode root = jp.getCodec().readTree(jp);
JsonNode nodeType = root.get("type");
String name = nodeType.isObject() ? nodeType.get("name").asText() : nodeType.asText();
return MyType.valueOf(name);
}
}
Then you can annotate your MyType enum with the #JsonDeserialize(using = MyTypeDeserializer.class) deleting your deserialize method like below :
#JsonDeserialize(using = MyTypeDeserializer.class)
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum MyType { //omitted fields and methods for brevity }
Context
Say you have:
public class Dto {
private String name;
private String List<Custom> customs;
// getters and setters...
}
and
public class Custom {
private String something;
private String else;
// getters and setters...
}
Your Spring MVC RestController receives a list of Dto:
#PostMapping
public String create(#RequestBody #Valid List<Dto> dtos) {
return myService.process(features);
}
Input
However, you know that the client-side service which will send data to your controller will send something like this:
[
{
"name": "Bob",
"customs": [
"{\n \"something\": \"yes\",\n \"else\": \"no\"\n }"
]
}
]
Notice how the List<Custom> actually ends up being received as a List<String>. Please assume this cannot be changed on the client-side and we have to deal with it on the server-side.
Question
Is there a Jackson annotation which would automagically take the input String and try to serialize it into a Custom class?
Attempts
A few things that didn't work, including:
#JsonSerialize(using = ToStringSerializer.class)
private List<Custom> customs;
along with
public Custom(String json) {
try {
new ObjectMapper().readerFor(Custom.class).readValue(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
As it is, we have had to change the customs type to List<String> and add a utility method which converts a String into a Custom using an ObjectMapper. This is rather dissatisfying.
You need to implement custom deserialiser or converter which would be used to convert given payload to required type. One trick, you could use is to create new ObjectMapper and use it for internal deserialisation.
Example usage:
class CustomConverter extends StdConverter<String, Custom> {
private final ObjectMapper mapper = new ObjectMapper();
#Override
public Custom convert(String value) {
try {
return mapper.readValue(value, Custom.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(value);
}
}
}
class Dto {
private String name;
#JsonDeserialize(contentConverter = CustomConverter.class)
private List<Custom> customs;
}
You need to create a custom Deserializer.
public class CustomDeserializer extends StdDeserializer<Custom> {
public CustomDeserializer() {
this(null);
}
public CustomDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Custom deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
int id = (Integer) ((IntNode) node.get("id")).numberValue();
String name = node.get("name").asText();
...
return new Custom(id, name, ...);
}
}
and register the deserializer on the Custom class:
#JsonDeserialize(using = CustomDeserializer.class)
public class Custom {
...
}
Assuming I have the following JSON:
{
"property": "123:1234"
}
How do I use Jackson annotations to ensure that the string value of "property" is de-serialized to a self-defined class rather than a String object?
I went through their documentation and I was unable to find this particular feature.
Thanks in advance.
You could create custom deserializer for your field. Assuming you want to map it to SomeClass object :
public class SomeClass {
#JsonDeserialize(using = CustomPropertyDeserializer.class)
private Properties property;
public Properties getProperty() {
return property;
}
public void setProperty(Properties property) {
this.property = property;
}
}
You annotate your field that you want to deserialize customly with #JsonDeserialize annotation passing custom deserializer.
Your deserializer could look like this :
public class CustomPropertyDeserializer extends StdDeserializer<Properties> {
public CustomPropertyDeserializer() {
super(Properties.class);
}
#Override
public Properties deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String valueAsString = p.getValueAsString();
String[] split = valueAsString.split(":");
return new Properties(split[0], split[1]);
}
}
And custom property class :
public class Properties {
private String first;
private String second;
public Properties(String first, String second) {
this.first = first;
this.second = second;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getSecond() {
return second;
}
public void setSecond(String second) {
this.second = second;
}
}
For testing it :
public static void main(String[] args) throws IOException {
String s = Files.lines(Paths.get("src/main/resources/data.json")).collect(Collectors.joining());
ObjectMapper objectMapper = new ObjectMapper();
SomeClass someClass = objectMapper.readValue(s, SomeClass.class);
System.out.println(someClass.getProperty().getFirst());
System.out.println(someClass.getProperty().getSecond());
}
The output is then :
123
1234
So all the custom logic how to map your String to some class that you define could be placed in deserialize method of your custom deserializer.
First thing first define your class that needs to be used:
#JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonTest{
#JsonProperty("property")
private String property;
//define your getters and setters for the field
Then you can use the ObjectMapper class from jackson:
public static <T> T extractObjectFromJson(String jsonText, Class<T> type) {
try {
return new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).reader().forType(type)
.readValue(jsonText);
} catch (Exception e) {
//Manage your exception here
}
return null;
}
So you can just call the method extractobjectFromJson(//Your JSON String, JsonTest.class) to get your JSON deserialized.