Deserialize a Map from JAXB generated XML using Jackson - java

I need to deserialize some XMLs that are being generated using JAXB.
Due to some compliance issue, I have to only use Jackson for XML parsing.
I am getting below exception when trying to deserialize a class that has a Map
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token
at [Source: (StringReader); line: 40, column: 21] (through reference chain: FileConfig["otherConfigs"]->java.util.LinkedHashMap["entry"])
My code is as follows:
XML file:
.
.
.
<fileConfig>
<whetherNotify>false</whetherNotify>
<url>....some location....</url>
<includes>app.log</includes>
<fileType>...some string...</fileType>
<otherConfigs>
<entry>
<key>pathType</key>
<value>1</value>
</entry>
</otherConfigs>
</fileConfig>
.
.
.
FileConfig.java
public class FileConfig implements Serializable {
protected Boolean whetherNotify = false;
protected String url;
protected String includes;
protected FileType fileType;
private Map<String, String> otherConfigs = new HashMap<String, String>();
....getters and setters.....
}
Main.java
public class Main {
.
.
.
.
private static <T> T unmarshallFromXML(String xml, Class<T> parseToClass) throws IOException {
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
xmlMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
xmlMapper.enable(SerializationFeature.INDENT_OUTPUT);
xmlMapper.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
xmlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
xmlMapper.setDefaultUseWrapper(false);
xmlMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
T parsedObject = xmlMapper.readValue(xml, parseToClass);
return parsedObject;
}
}
Please suggest a method to successfully parse that map using Jackson.

By default Map is serialized to XML in:
...
<key>value</key>
<key1>value1</key1>
...
format. There is not entry element. You have two options:
Change model and instead of Map use List<Entry> type.
Implement custom deserializer for a Map type.
New model
You need to create Entry class:
class Entry {
private String key;
private String value;
// getters, setters, toString
}
and change property in FileConfig class to:
List<Entry> otherConfigs;
Custom map deserializer
See below example:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class XmlApp {
public static void main(String[] args) throws Exception {
File xmlFile = new File("./test.xml");
XmlMapper xmlMapper = new XmlMapper();
FileConfig fileConfig = xmlMapper.readValue(xmlFile, FileConfig.class);
System.out.println(fileConfig);
}
}
class MapEntryDeserializer extends JsonDeserializer<Map<String, String>> {
#Override
public Map<String, String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Map<String, String> map = new HashMap<>();
JsonToken token;
while ((token = p.nextToken()) != null) {
if (token == JsonToken.FIELD_NAME) {
if (p.getCurrentName().equals("entry")) {
p.nextToken();
JsonNode node = p.readValueAsTree();
map.put(node.get("key").asText(), node.get("value").asText());
}
}
}
return map;
}
}
class FileConfig {
protected Boolean whetherNotify = false;
protected String url;
protected String includes;
#JsonDeserialize(using = MapEntryDeserializer.class)
private Map<String, String> otherConfigs;
// getters, setters, toString
}
Above code prints:
FileConfig{whetherNotify=false, url='....some location....', includes='app.log', otherConfigs={pathType1=2, pathType=1}}

Related

Value of a json key is another valid json itsef. Can this value be parsed as a String instead of parsing the inner json as json?

I have the following code which should parse a simple json as follows:
{"jsonTest":
{"innerKey":"innerValue"}
}
Here, the value of 'jsonTest' is a json object.
Question : Can this JSON be parsed (using jackson) and retrieve the value of 'jsonTest' as a simple String? In other words, is there a way to ask the parser not to parse the inner json object?
here is what I tried so far. This results in a parsing exception
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonParserTest {
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
String json = "{\"jsonTest\":{\"innerKey\":\"innerValue\"}}";
TestJson js = mapper.readValue(json, TestJson.class);
System.out.println(js.getJsonTest());
}
}
class TestJson {
private String jsonTest;
public String getJsonTest() {
return jsonTest;
}
public void setJsonTest(String jsonTest) {
this.jsonTest = jsonTest;
}
}
You can use JsonNode for this. Just change your TestJson class:
class TestJson {
private JsonNode jsonTest;
public JsonNode getJsonTest() {
return jsonTest;
}
public void setJsonTest(JsonNode jsonTest) {
this.jsonTest = jsonTest;
}
}
Output:
{"innerKey":"innerValue"}
Also you can do it just using the JsonNode without the TestJson class.
Use the following mapper.readTree method:
JsonNode jsonNode = mapper.readTree(json);
System.out.println(jsonNode.get("jsonTest").toString());
You should create structure to deseralize that json. It could be something like this:
#Data
#NoArgsConstructor
class TestJson {
JsonTest jsonTest;
#Data
#NoArgsConstructor
private static class JsonTest {
String innerKey;
}
}

jackson xmlmapper for map <String, Object>, react for specific tag

I have this xml structure:
<Result>
<ReferenceMin>4</ReferenceMin>
<ReferenceMax>5.65</ReferenceMax>
<PrecedingValue>3.25</PrecedingValue>
<PrecedingDate><Date year="2017" month="04" day="21"/></PrecedingDate>
</Result>
This xml come from 3-rd party service which can not be controlled, and it can contain new fields, or existing fields can disappear, so I can't define strict structure for object. As I can see, all fields can be parsed as "String" except PrecedingDate.
Is it possible to teach jackson xmlMapper work with PrecedingDate or Date field by my cyustom strategy? Currently it create objects with this structure:
{PrecedingDate: Date: {year: 2017, month: 04, day: 21}}
I want to get java date or instant or something similar.
You can implement custom deserialiser or use JsonAnySetter annotation. How to use annotation you can find below:
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
public class XmlMapperApp {
public static void main(String[] args) throws Exception {
File xmlFile = new File("./resource/test.xml").getAbsoluteFile();
XmlMapper xmlMapper = new XmlMapper();
System.out.println(xmlMapper.readValue(xmlFile, Result.class));
}
}
class Result {
private Map<String, String> entries = new HashMap<>();
private LocalDate precedingDate;
public Map<String, String> getEntries() {
return entries;
}
public LocalDate getPrecedingDate() {
return precedingDate;
}
#JsonAnySetter
public void setEntry(String key, Object value) {
if ("PrecedingDate".equals(key)) {
Map<String, String> date = (Map<String, String>)((Map) value).get("Date");
precedingDate = LocalDate.of(
Integer.parseInt(date.get("year")),
Integer.parseInt(date.get("month")),
Integer.parseInt(date.get("day")));
} else {
entries.put(key, value.toString());
}
}
#Override
public String toString() {
return "Result{" +
"entries=" + entries +
", precedingDate=" + precedingDate +
'}';
}
}
Above code prints:
Result{entries={ReferenceMin=4, PrecedingValue=3.25, ReferenceMax=5.65}, precedingDate=2017-04-21}

"Relaxed" fields names for Jackson

I'm working on Jackson configuration and I wonder if there is any option to deserialise different kinds of field patterns.
For example, I have an object:
class DeserializeIt {
String fieldOne;
String fieldOneAndHalf;
String fieldTwo;
String fieldThree;
String fieldFour;
//getters setters etc.
}
And I have below JSON payload:
{
"fieldOne" : "value1",
"field_ONE-and_Half": "value15",
"FIELD_TWO": "value2",
"FIELD_THREE" : "value3",
"field_four": "value4"
}
I would like to deserialize all these field names to camel case without an exception.
I tried to create my custom PropertyNamingStrategy but it goes from another direction: it does not convert delimitered fields to camel case, it tries to convert the objects fields and search for them in the parsed string.
And since I cannot pass a list of possible strings instead of one variation (fieldOne can become field-one, field_one, field-ONE etc.), this does not work.
Do you know what else could I configure for such a relaxed deserialization?
We need to extend com.fasterxml.jackson.databind.deser.BeanDeserializerModifier and com.fasterxml.jackson.databind.deser.BeanDeserializer which deserialises POJO classes. Below solution depends from version you are using because I copied some code from base class which is not ready for intercepting extra functionality. If you do not have any extra configuration for your POJO classes vanillaDeserialize method will be invoked and this one we will try to improve.
In other case you need to debug this deserialiser and updated other places if needed. Below solution uses version 2.9.8.
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
SimpleModule relaxedModule = new SimpleModule();
relaxedModule.setDeserializerModifier(new RelaxedBeanDeserializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(relaxedModule);
System.out.println(mapper.readValue(jsonFile, DeserializeIt.class));
}
}
class RelaxedBeanDeserializerModifier extends BeanDeserializerModifier {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
JsonDeserializer<?> base = super.modifyDeserializer(config, beanDesc, deserializer);
if (base instanceof BeanDeserializer) {
return new RelaxedBeanDeserializer((BeanDeserializer) base);
}
return base;
}
}
class RelaxedBeanDeserializer extends BeanDeserializer {
private Map<String, String> properties = new HashMap<>();
public RelaxedBeanDeserializer(BeanDeserializerBase src) {
super(src);
_beanProperties.forEach(property -> {
properties.put(property.getName().toLowerCase(), property.getName());
});
}
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// common case first
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
// 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
// what it is, including "expected behavior".
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
protected Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException {
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
// [databind#631]: Assign current value, to be accessible by custom serializers
p.setCurrentValue(bean);
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName();
do {
String relaxedName = getRelaxedName(propName);
String mappedName = properties.get(relaxedName);
defaultImplementation(p, ctxt, bean, mappedName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}
private void defaultImplementation(JsonParser p, DeserializationContext ctxt, Object bean, String propName) throws IOException {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
return;
}
handleUnknownVanilla(p, ctxt, bean, propName);
}
private String getRelaxedName(String name) {
return name.replaceAll("[_\\-]", "").toLowerCase();
}
}
Above code prints:
DeserializeIt{fieldOne='value1', fieldOneAndHalf='value15', fieldTwo='value2', fieldThree='value3', fieldFour='value4'}
See also:
Can Jackson check for duplicated properties in a case insensitive way?
From Jackson 2.9, you can provide multiple possible properties names for deserialization using the
#JsonAlias annotation. On your example, it would be like this:
class DeserializeIt {
#JsonAlias("fieldOne")
String fieldOne;
#JsonAlias("field_ONE-and_Half")
String fieldOneAndHalf;
#JsonAlias("FIELD_TWO")
String fieldTwo;
#JsonAlias("FIELD_THREE")
String fieldThree;
// and so on...
}
What worked for myself: I added an AOP component that renames all the fields of incoming object into the Camel case.

want to map a json object unto a java object

I am using a certain API which have a json output as below. I have parsed it to String.
the json output as string: {"result":{"number":"INC0022500"}}.
As you can see it has nested object for key result.
my snippet which i am using to map the above json unto a object.
Gson gson = new Gson();
EspIncidentTrial staff = gson.fromJson(json, EspIncidentTrial.class);
ESPIncidentTrial class:
import javax.persistence.Embeddable;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
#Embeddable
#JsonIgnoreProperties(ignoreUnknown = true)
public class EspIncidentTrial {
#JsonProperty("result")
private ResultTrial result;
public ResultTrial getResult() {
return result;
}
public void setResult(ResultTrial result) {
this.result = result;
}
}
For the nested object i created another class ResultTrial. Below is the body.
ResultTrial class:
import javax.persistence.Embeddable;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
#Embeddable
#JsonIgnoreProperties(ignoreUnknown = true)
public class ResultTrial {
#JsonProperty("number")
private String incidentId;
public String getIncidentId() {
return incidentId;
}
public void setIncidentId(String incidentId) {
this.incidentId = incidentId;
}
}
What happens now is, in EspIncidentTrial class, object result is getting mapped. However, inside ResultTrial class, no mapping is being done.
I tried treating the key result in the json object as String, but the threw the below error, which was expected though.
The error occured while parsing the JSON. com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_OBJECT at line 1 column 12 path $.result
Please help!
Here you are mixing gson and Jackson.
You are using annoations of Jackson, but using GSON's method to deserailze it.
Use Jackson's objectMapper to deserialize.
E.g.:
ObjectMapper mapper = new ObjectMapper();
EspIncidentTrial staff = mapper.readValue(json, EspIncidentTrial.class);
you can try this....
String searchdata="{\"userPojo\":{\"userid\":1156}}";
JSONObject jsonObject=new JSONObject(searchdata);
SummaryPojo summaryPojo=new SummaryPojo();
if(searchdata.contains("userPojo"))
{
String jsonstring=jsonObject.getString("userPojo");
Gson gson = new Gson();
UserPojo userPojo = gson.fromJson(searchdata, UserPojo.class);
summaryPojo.setUserPojo(userPojo);
}

Jackson: Ignore whitespace in empty #XmlWrapperElement collection

Using Jackson and jackson-dataformat-xml 2.4.4, I'm trying to deserialize a XML document where a collection annotated with #XmlWrapperElement may have zero elements, but where the XML contains whitespace (in my case a line break). Jackson throws a JsonMappingException on this content with the message “Can not deserialize instance of java.util.ArrayList out of VALUE_STRING token”. I cannot change the way the XML is produced.
Example:
static class Outer {
#XmlElementWrapper
List<Inner> inners;
}
static class Inner {
#XmlValue
String foo;
}
ObjectMapper mapper = new XmlMapper().registerModules(new JaxbAnnotationModule());
String xml = "<outer><inners>\n</inners></outer>";
Outer outer = mapper.readValue(xml, Outer.class);
The following workarounds do not work:
Enabling DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY: In this case Jackson wants to instantiate a bogus instance of Inner using the whitespace as content.
Creating setters for this field for both String and the collection type. In this case I get a JsonMappingException (“Conflicting setter definitions for property "inners"”).
In a similar Stackoverflow question it is suggested to downgrade Jackson to 2.2.3. This does not fix the problem for me.
Any suggestions?
Edit: I can work around this issue by wrapping the CollectionDeserializer and checking for a whitespace token. This looks however very fragile to me, e.g. I had to override another method to rewrap the object. I can post the workaround, but a cleaner approach would be better.
A workaround for this problem is to wrap the standard CollectionDeserializer to return an empty collection for tokens containing whitespace and register the new Deserializer. I put the code into a Module so it can be registered easily:
import java.io.IOException;
import java.util.Collection;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.CollectionType;
public class XmlWhitespaceModule extends SimpleModule {
private static class CustomizedCollectionDeserialiser extends CollectionDeserializer {
public CustomizedCollectionDeserialiser(CollectionDeserializer src) {
super(src);
}
private static final long serialVersionUID = 1L;
#SuppressWarnings("unchecked")
#Override
public Collection<Object> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
if (jp.getCurrentToken() == JsonToken.VALUE_STRING
&& jp.getText().matches("^[\\r\\n\\t ]+$")) {
return (Collection<Object>) _valueInstantiator.createUsingDefault(ctxt);
}
return super.deserialize(jp, ctxt);
}
#Override
public CollectionDeserializer createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException {
return new CustomizedCollectionDeserialiser(super.createContextual(ctxt, property));
}
}
private static final long serialVersionUID = 1L;
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyCollectionDeserializer(
DeserializationConfig config, CollectionType type,
BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (deserializer instanceof CollectionDeserializer) {
return new CustomizedCollectionDeserialiser(
(CollectionDeserializer) deserializer);
} else {
return super.modifyCollectionDeserializer(config, type, beanDesc,
deserializer);
}
}
});
}
}
After that you can add it to your ObjectMapper like this:
ObjectMapper mapper = new XmlMapper().registerModule(new XmlWhitespaceModule());

Categories

Resources