I need to write some Java code with these requirements:
- Input is received as a
Map<String, String>
. - The code needs to use Jackson to convert the input into a POJO. Some of the POJO's fields contain strings and enums. For these fields, the map's values are simply the plain strings or the result of calling
.toString()
on an enum value. Other fields contain other POJOs, lists of POJOs, etc. These exist as JSON objects in the map. - Custom code needs to be kept to a minimum. Ideally, annotations in the POJO classes would contain all the information Jackson needs to do its job. (If a new field is added to a POJO class with the proper annotations, it should work without additional code changes.)
- The code needs to use an
ObjectMapper
that's provided using dependency injection. This limits the places in the code that can use theObjectMapper
. (For example, it's not available in a@JsonSetter
method in the POJO class.)
Sample Classes:
public enum Color {
red,
blue
}
@Data
@Builder
public class MyPojo {
private String name;
private Color color;
private List<String> words;
private MyOtherPojo properties;
}
@Data
@Builder
public class MyOtherPojo {
private String text;
private String moreText;
private MyOtherPojo moreProperties;
}
Sample input:
Map<String, String> input = Map.of(
"name", "foo",
"color", "blue",
"words", "[\"banana\", \"desk\", \"cloud\"]",
"properties", "{\"text\": \"sample text\", \"moreText\": \"stuff\", \"moreProperties\": {\"text\": \"Wahoo!\"}}"
);
I have it partly working. My conversion code looks like this:
public class MyPojoConverter {
private final ObjectMapper objectMapper;
public MyPojoConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public MyPojo deserialize(Map<String, String> input) {
return objectMapper.convertValue(input, MyPojo.class);
}
}
This implementation works fine for String
and enum fields, but not for fields that contain other POJOs. If MyPojo
has a field of type MyOtherPojo
, I get an error like this:
java.lang.IllegalArgumentException: Cannot construct instance of `MyOtherPojo` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value (...
It seems that Jackson wants a MyOtherPojo
constructor that takes a String
, but that class doesn't have such a constructor and I can't add one to it. I would prefer that Jackson automatically handle the other POJO recursively. Is there a way to make it do this?
I need to write some Java code with these requirements:
- Input is received as a
Map<String, String>
. - The code needs to use Jackson to convert the input into a POJO. Some of the POJO's fields contain strings and enums. For these fields, the map's values are simply the plain strings or the result of calling
.toString()
on an enum value. Other fields contain other POJOs, lists of POJOs, etc. These exist as JSON objects in the map. - Custom code needs to be kept to a minimum. Ideally, annotations in the POJO classes would contain all the information Jackson needs to do its job. (If a new field is added to a POJO class with the proper annotations, it should work without additional code changes.)
- The code needs to use an
ObjectMapper
that's provided using dependency injection. This limits the places in the code that can use theObjectMapper
. (For example, it's not available in a@JsonSetter
method in the POJO class.)
Sample Classes:
public enum Color {
red,
blue
}
@Data
@Builder
public class MyPojo {
private String name;
private Color color;
private List<String> words;
private MyOtherPojo properties;
}
@Data
@Builder
public class MyOtherPojo {
private String text;
private String moreText;
private MyOtherPojo moreProperties;
}
Sample input:
Map<String, String> input = Map.of(
"name", "foo",
"color", "blue",
"words", "[\"banana\", \"desk\", \"cloud\"]",
"properties", "{\"text\": \"sample text\", \"moreText\": \"stuff\", \"moreProperties\": {\"text\": \"Wahoo!\"}}"
);
I have it partly working. My conversion code looks like this:
public class MyPojoConverter {
private final ObjectMapper objectMapper;
public MyPojoConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public MyPojo deserialize(Map<String, String> input) {
return objectMapper.convertValue(input, MyPojo.class);
}
}
This implementation works fine for String
and enum fields, but not for fields that contain other POJOs. If MyPojo
has a field of type MyOtherPojo
, I get an error like this:
java.lang.IllegalArgumentException: Cannot construct instance of `MyOtherPojo` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value (...
It seems that Jackson wants a MyOtherPojo
constructor that takes a String
, but that class doesn't have such a constructor and I can't add one to it. I would prefer that Jackson automatically handle the other POJO recursively. Is there a way to make it do this?
2 Answers
Reset to default 1Problem Explanation
In your code, you're using convertValue
to create an instance of MyPojo
. This method performs a two-step conversion, where the input
map is first serialized into a Json and then deserialized into the target class (MyPojo
).
Convenience method for doing two-step conversion from given value, into instance of given value type [...] This method is functionally similar to first serializing given value into JSON, and then binding JSON data into value of given type.
This means that the input
map will first be represented in a Json format and then converted into an instance of MyPojo
. However, since each property of MyPojo
is represented as a String
in the given Map
, input
contains also a string representation of the field MyOtherPojo
. This String
value is used by Jackson to create an instance of MyOtherPojo
, but because the class does not exhibit a constructor with a String
argument, Jackson throws an IllegalArgumentException
.
no String-argument constructor/factory method to deserialize from String value
You can test this behavior by commenting/uncommenting the String
constructor in the following example:
public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("i", "3");
map.put("otherPojo", new MyOtherPojo(1, "test").toString());
ObjectMapper mapper = new ObjectMapper();
MyPojo pojo = mapper.convertValue(map, MyPojo.class);
System.out.println(pojo);
}
@Data
@NoArgsConstructor
static class MyPojo {
private int i;
private MyOtherPojo otherPojo;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class MyOtherPojo {
private int x;
private String s;
//This is a bad practice. DO NOT initialize an object from its string representation as this is subject to change.
public MyOtherPojo(String toString) {
//... fetching properties ...
}
}
}
Possible Solutions
In this scenario, you either need to provide a String
constructor for the class MyOtherPojo
which extracts the object's properties from the given string (not recommended), or pass to your deserialize
method a Json String
, instead of a Map<String, String>
, and supply it to the readValue()
method (not convertValue()
).
Alternatively, if you cannot change the implementation of MyOtherPojo
(as you've specified), or cannot pass a String
Json in place of Map<String, String>
, you can define a custom deserializer to create an instance of MyOtherPojo
from a string, and register it as a module on the ObjectMapper
.
public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("i", "3");
map.put("otherPojo", new MyOtherPojo(1, "test").toString());
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(MyOtherPojo.class, new MyOtherPojoDeserializer());
mapper.registerModule(module);
MyPojo pojo = mapper.convertValue(map, MyPojo.class);
System.out.println(pojo);
}
static class MyOtherPojoDeserializer extends StdDeserializer<MyOtherPojo> {
public MyOtherPojoDeserializer() {
this(null);
}
public MyOtherPojoDeserializer(Class<?> vc) {
super(vc);
}
@Override
public MyOtherPojo deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
MyOtherPojo otherPojo = new MyOtherPojo();
//... deserialization logic ...
return otherPojo;
}
}
@Data
@NoArgsConstructor
static class MyPojo {
private int i;
private MyOtherPojo otherPojo;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class MyOtherPojo {
private int x;
private String s;
}
}
I figured out a simple solution that doesn't require writing custom deserializers for every type of POJO.
public class MyPojoConverter {
private static final Pattern JsonObjectOrArrayPattern = Patternpile("^(\\{.*}|\\[.*])$");
private final ObjectMapper objectMapper;
public MyPojoConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
private Object convertEntryToObject(Map.Entry<String, String> entry) throws JsonProcessingException {
String value = entry.getValue();
if (JsonObjectOrArrayPattern.matcher(value).matches()) {
return objectMapper.readValue(value, Object.class);
}
return value;
}
public MyPojo deserialize(Map<String, String> input) throws JsonProcessingException {
Map<String, Object> convertedAttributes = attributes.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, this::convertEntryToObject));
return objectMapper.convertValue(convertedAttributes, MyPojo.class);
}
}
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745588813a4634709.html
Map<String, String>
— do you mean you're receiving a JSON object mapping String names to String values? or String names to arbitrary other things? (which would not be a Map<String,String>) When you say "Other fields contain other POJOs" — that can't be represented as a simple String-String map. Are you saying you have an existing POJO that you want to map some JSON into? ... one tip - you can always write a completely custom deserializer that can do pretty much anything you want. – Stephen P Commented Nov 19, 2024 at 0:14Map<String, String>
as shown in the MyPojoConverter class. The map's values are all Strings. The values that correspond to other POJOs are JSON representations of those objects. – mrog Commented Nov 19, 2024 at 0:17