java - How to convert a Map<String, String> to a POJO using Jackson? - Stack Overflow

I need to write some Java code with these requirements:Input is received as a Map<String, String>

I need to write some Java code with these requirements:

  1. Input is received as a Map<String, String>.
  2. 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.
  3. 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.)
  4. The code needs to use an ObjectMapper that's provided using dependency injection. This limits the places in the code that can use the ObjectMapper. (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:

  1. Input is received as a Map<String, String>.
  2. 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.
  3. 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.)
  4. The code needs to use an ObjectMapper that's provided using dependency injection. This limits the places in the code that can use the ObjectMapper. (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?

Share Improve this question edited Nov 20, 2024 at 13:23 mrog asked Nov 18, 2024 at 23:50 mrogmrog 2,1344 gold badges24 silver badges29 bronze badges 3
  • You say you're receiving input as a 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:14
  • The input is literally a Map<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
  • 1 Show a minimum, reproducible example, and your attempt to solve the problem. – Abhijit Sarkar Commented Nov 19, 2024 at 1:17
Add a comment  | 

2 Answers 2

Reset to default 1

Problem 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

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信