Jackson deserialize Yaml Map when it is empty - java

I am trying to deserialize YAML map to Java Map of simple String keys and values for these cases:
myMap:
key: value
myMap:
# key: value
Here's the Java Pojo to create after deserialization:
class Config {
#JsonProperty Map<String, String> myMap;
public Config() {}
public Config(Map<String, String> myMap) { this.myMap = myMap; }
public Map<String, String> getMyMap() { return myMap; }
#Override
public String toString() {
return "Config{" + "map=" + myMap + '}';
}
}
And the main is :
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
String yaml = ".."
Config config = mapper.readValue(yaml, Config.class);
System.out.println(config);
}
The project uses these dependencies:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.1.4</version>
</dependency>
In case of yaml = "myMap:\n" + " key: somevalue", the deserialization works fine, and we can see Config{myMap={key=somevalue}}
In case of yaml = "myMap:\n" + ""; (empty or commented entries), Jackson logs these errors:
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [map type; class java.util.LinkedHashMap, [simple type, class java.lang.String] -> [simple type, class java.lang.String]] from String value; no single-String constructor/factory method (through reference chain: com.example.Config["myMap"]) ...
How can I do to manage the case where the lines are commented or there are no provided key/value pairs?

A YAML block-style map, like the one you use, is implicitly started with the first key/value pair. Therefore, in absence of key/value pairs, no map is started – the value of myMap will be the empty scalar. An empty scalar is loaded as empty string by Jackson, which leads to the error.
To mitigate this, you need to explicitly set the value to the empty map:
myMap: {}
{} is a flow-style empty map.

Related

Jackson: How to use a custom deserializer with #JsonAnySetter annotation?

I have several YAML config files I want to deserialize into a class. The YAML in the files consists of simple name value pairs with no nesting. There's a handful of properties that will have explicit fields, but the rest I just want dumped into a Map.
This all works fine, but I also want all the values of the properties that get deserialized into the Map through .add() to be run through a custom deserializer. I've tried using #JsonDeserialize on the setter value parameter and the setter method itself but Jackson seems to ignore it altogether.
Here's how it's set up:
public class ConfigData {
private Map<String, Object> dynamicConfig = new LinkedHashMap<>();
#JsonAnyGetter
public Map<String, Object> getConfig() {
return dynamicConfig;
}
#JsonAnySetter
public void add(String name, #JsonDeserialize(using = FooDeserializer.class) Object value) {
dynamicConfig.put(name, value);
}
#JsonProperty("some_special_property")
public String setSomeSpecialProperty(String value) {
add("some_special_property", value);
}
}
And to deserialize:
public static ConfigData getConfig(URL configResource) throws IOException {
try (InputStream stream = configResource.openStream()) {
ObjectMapper mapper = new YAMLMapper();
return mapper.readValue(new InputStreamReader(stream, StandardCharsets.UTF_8), ConfigData.class);
}
}
I discovered the problem was that I was specifying the deserializer class with the using property of the #JsonDeserialize annotation. For this specific use case I needed to use the contentUsing property instead, which is used for things like the value field of a Map entry.
This is what my setter looks like now:
#JsonAnySetter
#JsonDeserialize(contentUsing = FooDeserializer.class)
public void add(String name, Object value) {
dynamicConfig.put(name, value);
}
Now all the values will be serialized using FooDeserializer, except for "some_special_property" which has its own setter.

Java: Parsing JSON with unknown keys to Map

In Java, I need to consume JSON (example below), with a series of arbitrary keys, and produce Map<String, String>. I'd like to use a standard, long term supported JSON library for the parsing. My research, however, shows that these libraries are setup for deserialization to Java classes, where you know the fields in advance. I need just to build Maps.
It's actually one step more complicated than that, because the arbitrary keys aren't the top level of JSON; they only occur as a sub-object for prefs. The rest is known and can fit in a pre-defined class.
{
"al" : { "type": "admin", "prefs" : { "arbitrary_key_a":"arbitary_value_a", "arbitrary_key_b":"arbitary_value_b"}},
"bert" : {"type": "user", "prefs" : { "arbitrary_key_x":"arbitary_value_x", "arbitrary_key_y":"arbitary_value_y"}},
...
}
In Java, I want to be able to take that String, and do something like:
people.get("al").get("prefs"); // Returns Map<String, String>
How can I do this? I'd like to use a standard well-supported parser, avoid exceptions, and keep things simple.
UPDATE
#kumensa has pointed out that this is harder than it looks. Being able to do:
people.get("al").getPrefs(); // Returns Map<String, String>
people.get("al").getType(); // Returns String
is just as good.
That should parse the JSON to something like:
public class Person {
public String type;
public HashMap<String, String> prefs;
}
// JSON parsed to:
HashMap<String, Person>
Having your Person class and using Gson, you can simply do:
final Map<String, Person> result = new Gson().fromJson(json, new TypeToken<Map<String, Person>>() {}.getType());
Then, retrieving prefs is achieved with people.get("al").getPrefs();.
But be careful: your json string is not valid. It shouldn't start with "people:".
public static <T> Map<String, T> readMap(String json) {
if (StringUtils.isEmpty(json))
return Collections.emptyMap();
ObjectReader reader = new ObjectMapper().readerFor(Map.class);
MappingIterator<Map<String, T>> it = reader.readValues(json);
if (it.hasNextValue()) {
Map<String, T> res = it.next();
return res.isEmpty() ? Collections.emptyMap() : res;
}
return Collections.emptyMap();
}
All you need to do next, it that check the type of the Object. If it is Map, then you have an object. Otherwise, this is a simple value.
You can use Jackson lib to achieve this.
Put the following in pom.xml.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
Refer the following snippet that demonstrates the same.
ObjectMapper mapper = new ObjectMapper();
HashMap<String, Object> people = mapper.readValue(jsonString, new TypeReference<HashMap>(){});
Now, it is deserialized as a Map;
Full example:
import java.io.IOException;
import java.util.HashMap;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class testMain {
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
String json = "{\"address\":\"3, 43, Cashier Layout, Tavarekere Main Road, 1st Stage, BTM Layout, Ambika Medical, 560029\",\"addressparts\":{\"apartment\":\"Cashier Layout\",\"area\":\"BTM Layout\",\"floor\":\"3\",\"house\":\"43\",\"landmark\":\"Ambika Medical\",\"pincode\":\"560029\",\"street\":\"Tavarekere Main Road\",\"subarea\":\"1st Stage\"}}";
ObjectMapper mapper = new ObjectMapper();
HashMap<String, Object> people = mapper.readValue(json, new TypeReference<HashMap>(){});
System.out.println(((HashMap<String, String>)people.get("addressparts")).get("apartment"));
}
}
Output: Cashier Layout

How can I construct a json with arrays and objects in java

I need a json in the following structure to pass as request body to my service.
{
"i":[
{
"a":{
"o1":"str1";
"o2":234;
}
}
]
}
I can either pass a map like
<"i[0].a.o1", "str1">
<"i[0].a.02", 345>
or pass them as string like
"i[0].a.o1"="str1"&"i[0].a.02"=345
to construct the Json.
How can I convert the map or string input to the json structure above?
Should I use inner classes for 'I' and 'A' and just use GsonUtils.getString(I)?
I agree with the answer https://stackoverflow.com/a/45367328/2735286: the Jackson library provides very nice features for serializing, deserializing objects to and from JSON.
Here is some sample code which creates the map structure you proposed, serializes it and then deserializes and checks, if the deserialized structure is the same as the original one.
public class JacksonMapperTest {
private static ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws IOException {
Map<String, Object> root = provideMap(); // Create map
String json = convertToJson(root); // Convert to string
System.out.println(json); // Print json
Map<String, Object> rootClone = convertToMap(json); // Convert string to map
System.out.println(root.equals(rootClone)); // Check, if the original and deserialized objects are the same.
}
private static Map<String, Object> convertToMap(String json) throws IOException {
TypeReference<HashMap<String,Object>> typeRef
= new TypeReference<HashMap<String,Object>>() {};
return mapper.readValue(json, typeRef);
}
private static String convertToJson(Map<String, Object> root) throws JsonProcessingException {
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(root);
}
private static Map<String, Object> provideMap() {
Map<String, Object> root = new HashMap<>();
List<Map<String, Object>> i = new ArrayList<>();
root.put("i", i);
Map<String, Object> element = new HashMap<>();
Map<String, Object> a = new HashMap<String, Object>() {
{
put("o1", "str1");
put("o2", 234);
}
};
i.add(element);
element.put("a", a);
return root;
}
}
If you run this code this is what you will see in the console:
{
"i" : [ {
"a" : {
"o1" : "str1",
"o2" : 234
}
} ]
}
true
If you want to used Jackson in your project, you can create a Maven project with these dependencies in the pom.xml file:
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
For more infos on Jackson, check: https://github.com/FasterXML/jackson
perhaps you should try jackson api. believe me, it will make your life way easier.
using this api, you can guarantee that you have a valid json object. also, you can easily build your json nodes.

Spring MVC #RequestBody map Optional<Enum>

I have a rest controller with this method:
#RequestMapping(value = "", method = { RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE })
public ResponseEntity<?> add(#Valid #RequestBody MyModel myModel, Errors errors) {
...
return new ResponseEntity<SomeObject>(someObject, HttpStatus.OK);
}
In MyModel has a field isMeetingOrSale that is enum (MeetingSaleFlag):
public enum MeetingSaleFlag {
MEETING("MEETING"),
SALE("SALE");
private final String name;
private MeetingSaleFlag(String s) { name = s; }
public boolean equalsName(String otherName) {
return (otherName == null) ? false : name.equals(otherName);
}
public String toString() { return this.name; }
}
and it can map a json that has a field "isMeetingOrSale" : "MEETING"
but the value in the json can be "isMeetingOrSale" : "" or completely missing, so in that case I want the field to be mapped to null. If I change the filed to be Optional<MeetingSaleFlag>
I got
Could not read JSON: Can not instantiate value of type [simple type,
class java.util.Optional<MeetingSaleFlag>] from String value
('MEETING'); no single-String constructor/factory method\\n at
[Source: java.io.PushbackInputStream#32b21158; line: 17, column: 18]
(through reference chain: MyModel[\"isMeetingOrSale\"]);
So the question is how can I map Optional enum from json?
Thanks to Sotirios Delimanolis's comment I was able to resolve the issue.
1) Add
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
as a dependency.
2) Reconfigure the Jackson mapper. Register:
#Bean
#Primary
public ObjectMapper jacksonObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());
return mapper;
}
OR do this to register the jdk8 module
/**
* #return Jackson jdk8 module to be registered with every bean of type
* {#link ObjectMapper}
*/
#Bean
public Module jdk8JacksonModule() {
return new Jdk8Module();
}
Another way to customize Jackson is to add beans of type com.fasterxml.jackson.databind.Module to your context. They will be registered with every bean of type ObjectMapper, providing a global mechanism for contributing custom modules when you add new features to your application.
Doing this will only register the additional module and keep the built-in Jackson configuration provided by Spring Boot.
3) result
Now when the property is missing from the sent json, it's mapped to null
(This is not that great. I was expecting that it will give me an Optional and I will be able to use .isPresent()).
When it's an empty string ("isMeetingOrSale" : ""), Jackson returns an error:
Could not read JSON: Can not construct instance of
MyModel from String value '': value not
one of declared Enum instance names: [VAL1, VAL2]
which looks OK to me.
Useful links : Jackson jdk8 module, Spring MVC configure Jackson
This is an example from our codebase:
#NotNull // You probably don't want this
#JsonSerialize(using=CountrySerializer.class)
#JsonDeserialize(using=CountryDeserializer.class)
private CountryCode country;
where CountryCode is a complex enum (see nv-i18n) and these are the classes to (de)serialized from/to JSON:
public class CountrySerializer extends JsonSerializer<CountryCode> {
#Override
public void serialize(CountryCode value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeString(value.getAlpha3()); // Takes the Alpha3 code
}
public Class<CountryCode> handledType() { return CountryCode.class; }
}
and
public class CountryDeserializer extends JsonDeserializer<CountryCode> {
#Override
public CountryCode deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
// You can add here the check whether the field is empty/null
return CountryCode.getByCode(jp.getText());
}
}
You can easily replicate the same scenario using MeetingSaleFlag instead of CountryCode.

Jackson trying to deserialize json into a List of typed objects

I am trying to use Jackson to deserialize a property on a object which is a List of typed objects. and i get the following error when I try to do it
Can not instantiate value of type [map type; class java.util.HashMap, [simple type, class java.lang.String] -> [simple type, class java.lang.String]] from JSON String; no single-String constructor/factory method
So far I have the following but it does not seem to work.
Terms.class
#JsonDeserialize(as=JsonMapDeserializer)
private List<ObjectA> results = null; //ommitted getter and setters
My Deserializer class is as follows.
public class JsonMapDeserializer extends JsonDeserializer<List<ObjectA>> {
List<ObjectA> retMap = new ArrayList<ObjectA>();
TypeReference<HashMap<String,String>[]> typeRef = new TypeReference<HashMap<String,String>[]>() {};
#Override
public List<ObjectA> deserialize(JsonParser parser, DeserializationContext ctx)
throws IOException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
//read the json string into the map
HashMap<String, String>[] maps = mapper.readValue(parser, typeRef);
if(maps != null) {
for(HashMap<String, String> map : maps) {
ObjectA result = new ObjectA("id", map.get("id"));
retMap.add(result);
}
}
return retMap;
}
}
and I am using simple modules to add the deserializer as follows
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("safety", Version.unknownVersion());
module.addDeserializer(List.class, new JsonMapDeserializer());
mapper.registerModule(module);
The JSON string that I end up trying to desrialize is as follows
"SearchTerms":{"results":[{id":"1010","checked":"true"}] // there are other fields I have just omitted them
When I run the code to deserialize I get the following stack trace
org.codehaus.jackson.map.JsonMappingException: Can not instantiate value of type [map type; class java.util.HashMap, [simple type, class java.lang.String] -> [simple type, class java.lang.String]] from JSON String; no single-String constructor/factory method (through reference chain: com.model.search["searchTerm1"])
at org.codehaus.jackson.map.deser.std.StdValueInstantiator._createFromStringFallbacks(StdValueInstantiator.java:379)
at org.codehaus.jackson.map.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:268)
at org.codehaus.jackson.map.deser.std.MapDeserializer.deserialize(MapDeserializer.java:244)
at org.codehaus.jackson.map.deser.std.MapDeserializer.deserialize(MapDeserializer.java:33)
at org.codehaus.jackson.map.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:104)
at org.codehaus.jackson.map.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:18)
at org.codehaus.jackson.map.ObjectMapper._readValue(ObjectMapper.java:2695)
at org.codehaus.jackson.map.ObjectMapper.readValue(ObjectMapper.java:1294)
at com.az.rd.ke.json.JsonColumnMapDeserializer.deserialize(JsonColumnMapDeserializer.java:41)
at com.az.rd.ke.json.JsonColumnMapDeserializer.deserialize(JsonColumnMapDeserializer.java:27)
at org.codehaus.jackson.map.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:299)
at org.codehaus.jackson.map.deser.SettableBeanProperty$MethodProperty.deserializeAndSet(SettableBeanProperty.java:414)
at org.codehaus.jackson.map.deser.BeanDeserializer.
From looking at the stack trace and debugging because my module has been set up as follows
module.addDeserializer(List.class, new JsonMapDeserializer());
When deserializing it seems to complain as soon as it gets to the first property in my object which is a list, because searchTerm1 which it complains about is only a list of strings.
Can anybody please advise on how I would deserialize a list of typed objects, or how I could add the deserializer correctly. If I changed the adddeserializer method to
module.addDeserializer(List<ObjectA>.class, new JsonMapDeserializer());
it has compiler issues because the deserializer class is typed as List<ObjectA>.
it does look like the object you want to deserialise is a Map<String, List<Map<String,String>>>, not a List<Map<String,String>>.
Maybe try that instead?

Categories

Resources