0

For example our target type is Info with following definition

class Info {
  Person person;
  // a dozen of other properties
  // getters and setters
}

class Person {
  String name;
  int age;

  // getters and setters
}

and our Map is like Collections.singletonMap("person", "{\"name\": \"nick\", \"age\": 18}")

How can I convert this Map to Info object? (adding a constructor for Person or parsing value one by one is not viable because there are actually a dozen of properties like Person.)

Tried using jackson with objectMapper.convertValue but it throw a Exception with message no String-argument constructor/factory method to deserialize from String value

4
  • The value is just JSON, parse that part with a JSON parser. Commented Feb 6, 2020 at 7:53
  • @Sweeper the thing is I got plenty of properties like person, parse them one by one is not appropriate. Commented Feb 6, 2020 at 7:55
  • I see that you tried using Jackson, which is a JSON parser. Commented Feb 6, 2020 at 8:02
  • Yes, I would like to deserialize Info as a whole instead of deserializing its properties separately and inject them into info, because there are too many properties. Commented Feb 6, 2020 at 8:07

4 Answers 4

1

Jackson's ObjectMapper knows how to convert a Map into a POJO, but it cannot map string with JSON text into a POJO, so you first have to parse the JSON text into something generic (Map or JsonNode tree).

Basically, convert your Map<String, String> into a Map<String, JsonNode>.

Map<String, String> data = Collections.singletonMap("person", "{\"name\": \"nick\", \"age\": 18}");

ObjectMapper mapper = new ObjectMapper();
Map<String, Object> dataTree = new HashMap<>();
for (Entry<String, String> entry : data.entrySet())
    dataTree.put(entry.getKey(), mapper.readTree(entry.getValue()));
Info info = mapper.convertValue(dataTree, Info.class);

System.out.println("name = " + info.getPerson().getName());
System.out.println("age = " + info.getPerson().getAge());

Output

name = nick
age = 18
Sign up to request clarification or add additional context in comments.

3 Comments

This actually occurred to me , but its seems not very efficient, its much better if we can do this in one go like telling jackson: "when srcType is string and targetType is POJO, use objectMapper.readValue"
@NickAllen But that would require the POJO's to be aware of the format in which the input to the mapper is presented. --- Besides, the performance of creating JsonNode objects is minimal compared to the actual process of parsing the JSON text, so that's a non-issue. The memory requirement of creating JsonNode trees for all the Info properties might be an issue, but really, how much memory would that me for one Info object? I'd say that's a non-issue too. I think you're worrying about nothing. Beware premature optimization, so just go with the simple code here.
Just go through jackson's source code, customizing converting behavior like I said is way too complex, I will go with your approach.
1

If you really have a Map<String, String>, with the key being your field name and value being a json structure, there is no other chance than:

switch (map.getKey()) { 
    case "person": 
           Person person = mapper.readValue(map.getValue(), Person.class);
           info.setPerson(person);
           break;
    //TODO add your other Map-Keys here
}

But if you have a normal json structure like:

{
   "person": {
      "name": "nick",
      "age": 18
   }
}

Then you could simply:

Info info = mapper.readValue(json, Info.class);

4 Comments

Unfortunately we only have a Map<String, String> which is returned from jedis hgetall operation, this is really a headache.
Then you'd have to create a mapping class yourself, similar to the switch case above. Or use reflection to detect the field name, if the target field is always within the Info.class?
Use reflection is a good idea, I am trying to customize the valueSerialize used by jackson, if that didnt work maybe I will go with reflection.
Otherwise answer from @Andreas is also a nice idea and would certainly work too.
0

You can achieve this easily with Gson library which is created by Google for handling the json structures.

Map<String, String> data = Collections.singletonMap("person", "{\"name\": \"nick\", \"age\": 18}");
Person person = new Gson.fromJson(data.get("person"), Person.class);
Info info = new Info();
info.setPerson(person);

Comments

0

Okay, after going through source code, I came up with a solution:

First, customize a BeanDeserialzier

class StringBeanDeserializer extends BeanDeserializer {
    private ObjectMapper objectMapper;
    public StringBeanDeserializer(BeanDeserializerBuilder builder, BeanDescription beanDesc, BeanPropertyMap properties, Map<String, SettableBeanProperty> backRefs, HashSet<String> ignorableProps, boolean ignoreAllUnknown, boolean hasViews, ObjectMapper objectMapper) {
        super(builder, beanDesc, properties, backRefs, ignorableProps, ignoreAllUnknown, hasViews);
        this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper;
    }

    @Override
    public Object deserializeFromString(JsonParser p, DeserializationContext ctxt) throws IOException {
        if (this._beanType.isTypeOrSubTypeOf(CharSequence.class)) {
            return super.deserializeFromString(p, ctxt);
        }
        return objectMapper.readValue(p.getText(), this._beanType.getRawClass());
    }
}

Then write a Builder to produce that BeanDeserialzier

class StringBeanDeserializerBuilder extends BeanDeserializerBuilder {
    private ObjectMapper objectMapper;
    public StringBeanDeserializerBuilder(BeanDescription beanDesc, DeserializationContext ctxt) {
        super(beanDesc, ctxt);
    }

    public StringBeanDeserializerBuilder(BeanDescription beanDesc, DeserializationContext ctxt, ObjectMapper objectMapper) {
        this(beanDesc, ctxt);
        this.objectMapper = objectMapper;
    }

    protected StringBeanDeserializerBuilder(BeanDeserializerBuilder src) {
        super(src);
    }

    /**
     * Method for constructing a {@link BeanDeserializer}, given all
     * information collected.
     */
    public JsonDeserializer<?> build()
    {
        Collection<SettableBeanProperty> props = _properties.values();
        _fixAccess(props);
        BeanPropertyMap propertyMap = BeanPropertyMap.construct(props,
                _config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES),
                _collectAliases(props));
        propertyMap.assignIndexes();

        // view processing must be enabled if:
        // (a) fields are not included by default (when deserializing with view), OR
        // (b) one of properties has view(s) to included in defined
        boolean anyViews = !_config.isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION);
        if (!anyViews) {
            for (SettableBeanProperty prop : props) {
                if (prop.hasViews()) {
                    anyViews = true;
                    break;
                }
            }
        }

        // one more thing: may need to create virtual ObjectId property:
        if (_objectIdReader != null) {
            /* 18-Nov-2012, tatu: May or may not have annotations for id property;
             *   but no easy access. But hard to see id property being optional,
             *   so let's consider required at this point.
             */
            ObjectIdValueProperty prop = new ObjectIdValueProperty(_objectIdReader, PropertyMetadata.STD_REQUIRED);
            propertyMap = propertyMap.withProperty(prop);
        }

        return new StringBeanDeserializer(this,
                _beanDesc, propertyMap, _backRefProperties, _ignorableProps, _ignoreAllUnknown,
                anyViews, objectMapper);
    }
}

Finally write a Factory use that Builder

public class StringBeanDeserializeFactory extends BeanDeserializerFactory {
    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * Globally shareable thread-safe instance which has no additional custom deserializers
     * registered
     */
    public final static BeanDeserializerFactory instance = new StringBeanDeserializeFactory(
            new DeserializerFactoryConfig());

    public StringBeanDeserializeFactory(DeserializerFactoryConfig config) {
        super(config);
    }

    /**
     * Overridable method that constructs a {@link BeanDeserializerBuilder}
     * which is used to accumulate information needed to create deserializer
     * instance.
     */
    protected BeanDeserializerBuilder constructBeanDeserializerBuilder(DeserializationContext ctxt,
                                                                       BeanDescription beanDesc) {
        return new StringBeanDeserializerBuilder(beanDesc, ctxt, objectMapper);
    }
}

Example Usage:

        ObjectMapper objectMapper = new ObjectMapper(null, null, new DefaultDeserializationContext.Impl(StringBeanDeserializeFactory.instance));
        Map<String, String> map = ImmutableMap.of("person", objectMapper.writeValueAsString(new Person("nick", 25)),
                "raw", "test");
        System.out.println(objectMapper.convertValue(map, Data.class));

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.