My JSON looks like this:
{"typeName":"test","field":{"name":"42"}}
I have two deserializers. The first one (JsonDeserializer<EntityImpl>) will examine the JSON and extract a type information which is provided by the typeName property.
The second deserializer (JsonDeserializer<TestField>) is used to deserialize the field property. This deserializer needs to know the previously extracted typeName value in order to work correctly.
How can i pass-along the type information from one deserializer to other deserializers? I tried to use DeserializationContext but i don't know how to pass along the Context from deserializer A to B.
My current code looks like this:
EntityImpl.java:
package de.jotschi.test;
public class EntityImpl implements Entity {
private String typeName;
private TestField field;
public String getTypeName() {
return typeName;
}
public void setTypeName(String typeName) {
this.typeName = typeName;
}
public TestField getField() {
return field;
}
public void setField(TestField field) {
this.field = field;
}
}
TestField.java:
package de.jotschi.test;
public class TestField {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Test:
package de.jotschi.test;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import de.jotschi.test.EntityImpl;
import de.jotschi.test.TestField;
public class TestMapper2 {
private InjectableValues getInjectableValue() {
InjectableValues values = new InjectableValues() {
#Override
public Object findInjectableValue(Object valueId, DeserializationContext ctxt, BeanProperty forProperty, Object beanInstance) {
if ("data".equals(valueId.toString())) {
return new HashMap<String, String>();
}
return null;
}
};
return values;
}
#Test
public void testMapper() throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule idAsRefModule = new SimpleModule("ID-to-ref", new Version(1, 0, 0, null));
idAsRefModule.addDeserializer(EntityImpl.class, new JsonDeserializer<EntityImpl>() {
#Override
public EntityImpl deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Map<String, String> dataMap = (Map) ctxt.findInjectableValue("data", null, null);
System.out.println("Value: " + dataMap.get("test"));
ObjectCodec codec = jp.getCodec();
JsonNode node = codec.readTree(jp);
String type = node.get("typeName").asText();
dataMap.put("typeName", type);
// How to pass on type information to TestField deserializer? The context is not reused for the next deserializer.
// I assume that readValueAs fails since the codec.readTree method has already been invoked.
//return jp.readValueAs(EntityImpl.class);
// Alternatively the treeToValue method can be invoked in combination with the node. Unfortunately all information about the DeserializationContext is lost. I assume new context will be created.
// How to reuse the old context?
return codec.treeToValue(node, EntityImpl.class);
}
});
idAsRefModule.addDeserializer(TestField.class, new JsonDeserializer<TestField>() {
#Override
public TestField deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// Access type from context
Map<String, String> dataMap = (Map) ctxt.findInjectableValue("data", null, null);
System.out.println(dataMap.get("typeName"));
ObjectCodec codec = p.getCodec();
JsonNode node = codec.readTree(p);
return codec.treeToValue(node, TestField.class);
}
});
mapper.registerModule(idAsRefModule);
mapper.setSerializationInclusion(Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Setup the pojo
EntityImpl impl = new EntityImpl();
impl.setTypeName("test");
TestField testField = new TestField();
testField.setName("42");
impl.setField(testField);
// POJO -> JSON
String json = mapper.writeValueAsString(impl);
System.out.println(json);
// JSON -> POJO
Entity obj = mapper.reader(getInjectableValue()).forType(EntityImpl.class).readValue(json);
System.out.println(obj.getClass().getName());
}
}
My current solution is call the following mapper this way:
return mapper.setInjectableValues(getInjectableValue(dataMap)).treeToValue(obj, EntityImpl.class);
This way the previously loaded context data map is put into a new context that is used for the following parsing process.
Full example:
package de.jotschi.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class TestMapper2 {
private InjectableValues getInjectableValue(final Map<String, String> dataMap) {
InjectableValues values = new InjectableValues() {
#Override
public Object findInjectableValue(Object valueId, DeserializationContext ctxt, BeanProperty forProperty, Object beanInstance) {
if ("data".equals(valueId.toString())) {
return dataMap;
}
return null;
}
};
return values;
}
#Test
public void testMapper() throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule idAsRefModule = new SimpleModule("ID-to-ref", new Version(1, 0, 0, null));
idAsRefModule.addDeserializer(Entity.class, new JsonDeserializer<Entity>() {
#Override
public Entity deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Map<String, String> dataMap = (Map) ctxt.findInjectableValue("data", null, null);
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode obj = (ObjectNode) mapper.readTree(jp);
String type = obj.get("typeName").asText();
dataMap.put("typeName", type);
return mapper.setInjectableValues(getInjectableValue(dataMap)).treeToValue(obj, EntityImpl.class);
}
});
idAsRefModule.addDeserializer(TestField.class, new JsonDeserializer<TestField>() {
#Override
public TestField deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// Access type from context
Map<String, String> dataMap = (Map) ctxt.findInjectableValue("data", null, null);
System.out.println("Type name: " + dataMap.get("typeName"));
ObjectMapper mapper = (ObjectMapper) p.getCodec();
ObjectNode obj = (ObjectNode) mapper.readTree(p);
// Custom deserialisation
TestField field = new TestField();
field.setName(obj.get("name").asText());
// Delegate further deserialisation to other mapper
field.setSubField(mapper.setInjectableValues(getInjectableValue(dataMap)).treeToValue(obj.get("subField"), SubField.class));
return field;
}
});
mapper.registerModule(idAsRefModule);
mapper.setSerializationInclusion(Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Setup the pojo
EntityImpl impl = new EntityImpl();
impl.setTypeName("test");
TestField testField = new TestField();
testField.setName("42");
SubField subField = new SubField();
subField.setName("sub");
testField.setSubField(subField);
impl.setField(testField);
// POJO -> JSON
String json = mapper.writeValueAsString(impl);
System.out.println(json);
// JSON -> POJO
Entity obj = mapper.reader(getInjectableValue(new HashMap<String, String>())).forType(Entity.class).readValue(json);
assertNotNull("The enity must not be null", obj);
assertNotNull(((EntityImpl) obj).getField());
assertEquals("42", ((EntityImpl) obj).getField().getName());
assertNotNull(((EntityImpl) obj).getField().getSubField());
assertEquals("sub", ((EntityImpl) obj).getField().getSubField().getName());
System.out.println(obj.getClass().getName());
}
}
Related
Description
I'm new to Java AND Jackson and I try to save a java.time.duration to a JSON in a nice and readable hh:mm (hours:minutes) format for storing and retrieving.
In my project I use:
Jackson com.fasterxml.jackson.core:jackson-databind:2.14.1.
Jackson com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.1 for the support of the newer Java 8 time/date classes.
Minimum working example:
Consider following example class:
public class Book {
private Duration timeToComplete;
public Book(Duration durationToComplete) {
this.timeToComplete = durationToComplete;
}
// default constructor + getter & setter
}
If I try to serialize a book instance into JSON like in the following code section
public class JavaToJson throws JsonProcessingException {
public static void main(String[] args) {
// create the instance of Book, duration 01h:11min
LocalTime startTime = LocalTime.of(13,30);
LocalTime endTime = LocalTime.of(14,41);
Book firstBook = new Book(Duration.between(startTime, endTime));
// create the mapper, add the java8 time support module and enable pretty parsing
ObjectMapper objectMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.build()
.enable(SerializationFeature.INDENT_OUTPUT);
// serialize and print to console
System.out.println(objectMapper.writeValueAsString(firstBook));
}
}
it gives me the duration in seconds instead of 01:11.
{
"timeToComplete" : 4740.000000000
}
How would I change the JSON output into a hh:mm format?
What I tried until now
I thought about adding a custom Serializer/Deserializer (potentially a DurationSerializer?) during instantiation of the ObjectMapper but it seems I can't make the formatting work...
ObjectMapper objectMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
// add the custom serializer for the duration
.addModule(new SimpleModule().addSerializer(new DurationSerializer(){
#Override
protected DurationSerializer withFormat(Boolean useTimestamp, DateTimeFormatter dtf, JsonFormat.Shape shape) {
// here I try to change the formatting
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm");
return super.withFormat(useTimestamp, dtf, shape);
}
}))
.build()
.enable(SerializationFeature.INDENT_OUTPUT);
All it does is change it to this strange textual representation of the Duration:
{
"timeToComplete" : "PT1H11M"
}
So it seems I'm not completely off but the formatting is still not there. Maybe someone can help with the serializing/de-serializing?
Thanks a lot
hh:mm format is not supported by Jackson since Java does not recognise it by default. We need to customise serialisation/deserialisation mechanism and provide custom implementation.
Take a look at:
How to format a duration in java? (e.g format H:MM:SS)
Format a Milliseconds Duration to HH:MM:SS
Using some examples from linked articles I have created custom serialiser and deserialiser. They do not handle all possible cases but should do the trick for your requirements:
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.DurationSerializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalTime;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class DurationApp {
public static void main(String[] args) throws JsonProcessingException {
LocalTime startTime = LocalTime.of(13, 30);
LocalTime endTime = LocalTime.of(14, 41);
Book firstBook = new Book(Duration.between(startTime, endTime));
// create the mapper, add the java8 time support module and enable pretty parsing
ObjectMapper objectMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.addModule(new SimpleModule()
.addSerializer(Duration.class, new ApacheDurationSerializer())
.addDeserializer(Duration.class, new ApacheDurationDeserializer()))
.build()
.enable(SerializationFeature.INDENT_OUTPUT);
String json = objectMapper.writeValueAsString(firstBook);
System.out.println(json);
Book deserialisedBook = objectMapper.readValue(json, Book.class);
System.out.println(deserialisedBook);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
class Book {
#JsonFormat(pattern = "HH:mm")
private Duration duration;
}
class ApacheDurationSerializer extends DurationSerializer {
private final String apachePattern;
public ApacheDurationSerializer() {
this(null);
}
ApacheDurationSerializer(String apachePattern) {
this.apachePattern = apachePattern;
}
#Override
public void serialize(Duration duration, JsonGenerator generator, SerializerProvider provider) throws IOException {
if (Objects.nonNull(apachePattern) && Objects.nonNull(duration)) {
String value = DurationFormatUtils.formatDuration(duration.toMillis(), apachePattern);
generator.writeString(value);
} else {
super.serialize(duration, generator, provider);
}
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
JsonFormat.Value format = findFormatOverrides(prov, property, handledType());
if (format != null && format.hasPattern() && isApacheDurationPattern(format.getPattern())) {
return new ApacheDurationSerializer(format.getPattern());
}
return super.createContextual(prov, property);
}
private boolean isApacheDurationPattern(String pattern) {
try {
DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), pattern);
return true;
} catch (Exception e) {
return false;
}
}
}
class ApacheDurationDeserializer extends DurationDeserializer {
private final String apachePattern;
private final int numberOfColonsInPattern;
public ApacheDurationDeserializer() {
this(null);
}
ApacheDurationDeserializer(String apachePattern) {
this.apachePattern = apachePattern;
this.numberOfColonsInPattern = countColons(apachePattern);
}
#Override
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException {
if (Objects.nonNull(apachePattern)) {
String value = parser.getText();
if (this.numberOfColonsInPattern != countColons(value)) {
throw new JsonMappingException(parser, String.format("Pattern '%s' does not match value '%s'!", apachePattern, value));
}
if (numberOfColonsInPattern == 0) {
return Duration.ofSeconds(Long.parseLong(value.trim()));
}
String[] parts = value.trim().split(":");
return switch (parts.length) {
case 1 -> Duration.ofSeconds(Long.parseLong(value.trim()));
case 2 -> Duration.ofSeconds(TimeUnit.HOURS.toSeconds(Long.parseLong(parts[0]))
+ TimeUnit.MINUTES.toSeconds(Long.parseLong(parts[1])));
case 3 -> Duration.ofSeconds(TimeUnit.HOURS.toSeconds(Long.parseLong(parts[0]))
+ TimeUnit.MINUTES.toSeconds(Long.parseLong(parts[1]))
+ Long.parseLong(parts[2]));
default ->
throw new JsonMappingException(parser, String.format("Pattern '%s' does not match value '%s'!", apachePattern, value));
};
} else {
return super.deserialize(parser, context);
}
}
#Override
public Duration deserialize(JsonParser p, DeserializationContext ctxt, Duration intoValue) throws IOException {
return super.deserialize(p, ctxt, intoValue);
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
if (format != null && format.hasPattern() && isApacheDurationPattern(format.getPattern())) {
return new ApacheDurationDeserializer(format.getPattern());
}
return super.createContextual(ctxt, property);
}
private boolean isApacheDurationPattern(String pattern) {
try {
DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), pattern);
return true;
} catch (Exception e) {
return false;
}
}
private static int countColons(String apachePattern) {
return StringUtils.countMatches(apachePattern, ':');
}
}
Above code prints:
{
"duration" : "01:11"
}
Book(duration=PT1H11M)
I try to convert a JSON into an XML with the following code
final ObjectMapper objectMapper = new ObjectMapper();
final XmlMapper xmlMapper = new XmlMapper();
JsonNode jsonNode = objectMapper.readTree(jsonString);
String xmlString = xmlMapper
.writerWithDefaultPrettyPrinter()
.withRootName("rootname")
.writeValueAsString(jsonNode);
Basically it works. Does anyone know, how I can add a namespace to the serialized XML-attributes. I've no POJOs for the objects. The convert should generate from this
{
"Status" : "OK"
}
something like this:
<ns2:rootname xmlns:ns2="http://whatever-it-is.de/">
<ns2:state>OK</ns2:state>
</ns2:rootname>
just create a pojo and add jackson annotations, e.g.
#JacksonXmlProperty(localName="ns2:http://whatever-it-is.de/")
public class Status {
// ...
}
Or if you want to go without a pojo try a custom serializer which adds namespaces
https://www.baeldung.com/jackson-custom-serialization
You need to provide custom Json Node serialiser and use ToXmlGenerator. See below example:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import javax.xml.namespace.QName;
import java.io.IOException;
public class XmlMapperApp {
public static void main(String... args) throws Exception {
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.enable(SerializationFeature.INDENT_OUTPUT);
ObjectNode node = xmlMapper.createObjectNode()
.put("Status", "OK")
.set("node", xmlMapper.createObjectNode()
.put("int", 1)
.put("str", "str"));
SimpleModule module = new SimpleModule();
module.setSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (beanDesc.getType().getRawClass().equals(ObjectNode.class)) {
return new ObjectNodeJsonSerializer(serializer);
}
return super.modifySerializer(config, beanDesc, serializer);
}
});
xmlMapper.registerModule(module);
System.out.println(xmlMapper.writeValueAsString(node));
}
}
class ObjectNodeJsonSerializer extends JsonSerializer<JsonNode> {
private final JsonSerializer baseSerializer;
ObjectNodeJsonSerializer(JsonSerializer baseSerializer) {
this.baseSerializer = baseSerializer;
}
#Override
public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) gen;
xmlGenerator.setNextName(new QName("http://whatever-it-is.de/", "rootname", "anything"));
baseSerializer.serialize(value, gen, serializers);
}
}
Above example prints:
<wstxns1:rootname xmlns:wstxns1="http://whatever-it-is.de/">
<wstxns1:Status>OK</wstxns1:Status>
<wstxns1:node>
<wstxns1:int>1</wstxns1:int>
<wstxns1:str>str</wstxns1:str>
</wstxns1:node>
</wstxns1:rootname>
Underscore-java library can convert JSON to XML with namespace.
{
"ns2:rootname": {
"-xmlns:ns2": "http://whatever-it-is.de/",
"ns2:state": "OK"
},
"#omit-xml-declaration": "yes"
}
<ns2:rootname xmlns:ns2="http://whatever-it-is.de/">
<ns2:state>OK</ns2:state>
</ns2:rootname>
So I'm used to getting JSON objects from a given API/endpoint, e.g.:
{
"count": 5,
"results": [
{
"example": "test",
"is_valid": true
},
{
"example": "test2",
"is_valid": true
}
]
}
And in a custom deserializer that extends com.fasterxml.jackson.databind.deser.std.StdDeserializer, I know I can use the JsonParser object like so to get the base node to work off of, i.e.:
#Override
public ResultExample deserialize(JsonParser jp, DeserializationContext ctxt) {
JsonNode node = jp.getCodec().readTree(jp);
JsonNode count = node.get("count");
// code to put parsed objects into a ResultExample object...
}
However, I just encountered an API that simply returns an array of JSON objects, e.g.:
[
{
"example": "test",
"is_valid": true
},
{
"example": "test2",
"is_valid": true
},
]
So, I don't believe I can just parse this the same way as before. What would be the correct way to parse this using Jackson?
This may help you:
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Test {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String json = "[\r\n" + " {\r\n" + " \"example\": \"test\",\r\n" + " \"is_valid\": true\r\n"
+ " },\r\n" + " {\r\n" + " \"example\": \"test2\",\r\n" + " \"is_valid\": true\r\n"
+ " }\r\n" + "]";
Example[] ex = mapper.readValue(json, Example[].class);
}
}
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({ "example", "is_valid" })
class Example {
#JsonProperty("example")
private String example;
#JsonProperty("is_valid")
private Boolean isValid;
public String getExample() {
return example;
}
#JsonProperty("example")
public void setExample(String example) {
this.example = example;
}
#JsonProperty("is_valid")
public Boolean getIsValid() {
return isValid;
}
#JsonProperty("is_valid")
public void setIsValid(Boolean isValid) {
this.isValid = isValid;
}
}
When response is a JSON Object you can use default bean deserialiser. In case it is a JSON Array you can read it as array and create response object manually. Below you can find example custom deserialiser and BeanDeserializerModifier which is used to register custom deserialiser. BeanDeserializerModifier allows to use default deserialiser when JSON payload fits POJO model:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
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.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.CollectionType;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (beanDesc.getBeanClass() == Response.class) {
return new ResponseJsonDeserializer((BeanDeserializerBase) deserializer);
}
return super.modifyDeserializer(config, beanDesc, deserializer);
}
});
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
System.out.println(mapper.readValue(jsonFile, Response.class));
}
}
class ResponseJsonDeserializer extends BeanDeserializer {
private final BeanDeserializerBase baseDeserializer;
protected ResponseJsonDeserializer(BeanDeserializerBase src) {
super(src);
this.baseDeserializer = src;
}
#Override
public Response deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (p.currentToken() == JsonToken.START_OBJECT) {
return (Response) baseDeserializer.deserialize(p, ctxt);
}
if (p.currentToken() == JsonToken.START_ARRAY) {
CollectionType collectionType = ctxt.getTypeFactory().constructCollectionType(List.class, Item.class);
JsonDeserializer<Object> deserializer = ctxt.findRootValueDeserializer(collectionType);
List<Item> results = (List<Item>) deserializer.deserialize(p, ctxt);
Response response = new Response();
response.setCount(results.size());
response.setResults(results);
return response;
}
throw MismatchedInputException.from(p, Response.class, "Only object or array!");
}
}
class Response {
private int count;
private List<Item> results;
// getters, setters, toString
}
class Item {
private String example;
#JsonProperty("is_valid")
private boolean isValid;
// getters, setters, toString
}
Above code for JSON Object payload prints:
Response{count=5, results=[Item{example='test', isValid=true}, Item{example='test2', isValid=true}]}
And for JSON Array payload prints:
Response{count=2, results=[Item{example='test', isValid=true}, Item{example='test2', isValid=true}]}
Guess I should have just written the unit test before asking the question, but apparently you can just do it the same way. Only difference is the base node is a JsonArray which you have to iterate over. Thanks everyone who looked into this.
I wrote the following JsonSerializer to let Jackson serialize an array of integers into JSON:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class TalkIdsSerializer extends JsonSerializer<TalkIds> {
/**
* Serializes a TalkIds object into the following JSON string:
* Example: { "talk_ids" : [ 5931, 5930 ] }
*/
#Override
public void serialize(TalkIds talkIds, JsonGenerator jsonGenerator,
SerializerProvider provider)
throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeArrayFieldStart(TalkIds.API_DICTIONARY_KEY);
for (Integer talkId : talkIds.getTalkIds()) {
jsonGenerator.writeNumber(talkId);
}
jsonGenerator.writeEndArray();
jsonGenerator.writeEndObject();
}
}
The class is used here:
#JsonSerialize(using = TalkIdsSerializer.class)
public class TalkIds { /* ... */ }
I want test the behavior of the serializer and came up with the following:
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class TalkIdsSerializerTest {
protected final ArrayList<Integer> TALK_IDS =
new ArrayList<>(Arrays.asList(5931, 5930));
protected TalkIdsSerializer talkIdsSerializer;
#Before
public void setup() throws IOException {
talkIdsSerializer = new TalkIdsSerializer();
}
#Test
public void testSerialize() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonGenerator jsonGenerator =
new JsonFactory().createGenerator(stringWriter);
TalkIds talkIds = new TalkIds();
talkIds.add(TALK_IDS);
talkIdsSerializer.serialize(talkIds, jsonGenerator, null);
String string = stringWriter.toString(); // string is ""
assertNotNull(string);
assertTrue(string.length() > 0);
stringWriter.close();
}
}
However, nothing is written to the StringWriter. What am I doing wrong?
You need to flush() the generator
Method called to flush any buffered content to the underlying target (output stream, writer), and to flush the target itself as well.
http://fasterxml.github.io/jackson-core/javadoc/2.1.0/com/fasterxml/jackson/core/JsonGenerator.html#flush()
I had a similar requirement, to test a custom serializer. I used objectMapper to get the string directly(since you have already annotated TalkIds with JsonSerialize). You can get the json string from the object as follows
String json = new ObjectMapper().writeValueAsString(talkIds)
For me flush() changed nothing, so I changed the way to test it, in accordance with http://www.baeldung.com/jackson-custom-serialization.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.StringWriter;
//...
#Test
public void serialize_custom() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(MyCustomSerializer.class, myCustomSerializer);
objectMapper.registerModule(module);
StringWriter stringWriter = new StringWriter();
TalkIds talkIds = new TalkIds();
talkIds.add(TALK_IDS);
objectMapper.writeValue(stringWriter,wi);
assertTrue(stringWriter.toString().length() > 3);
}
I know there are similar questions around such as How to marshal/unmarshal a Map<Integer, List<Integer>>? and JAXB java.util.Map binding. Also I read Blaise Doughan's blog a lot especially this post: http://blog.bdoughan.com/2013/03/jaxb-and-javautilmap.html and tried to follow what he suggested as much as I can, however I still cannot unmarshal the json payload successfully and really appreciate your help.
The json payload to unmarshal looks like this:
{
"uri":"\\foo\\dosomthing",
"action":"POST",
"queryParameters":[
"$filter=aaa",
"$orderby=bbb"
],
"requestData":{
"data1":{
"key1":"value1",
"key2":"value2"
},
"ids":[
"1234",
"0294"
]
}
}
And I am having problem to unmarshal the "data" into the java.util.Map. The "data" field does not have specific schema so it can contains an array, key-value pairs or any other valid json data. I decided to use a Map to wrap it. Based on what I researched, I think I need XmlAdapter to convert the data properly.
Here are my code:
The Java Schema Class:
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.eclipse.persistence.oxm.annotations.XmlPath;
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public class CustomerRequest
{
public CustomerRequest() {}
public CustomerRequest(String uri, String action, List<String>
queryParameters, Map<String, Object> reqeustData)
{
this.uri = uri;
this.action = action;
this.queryParameters = queryParameters;
this.requestData = reqeustData;
}
public String getUri()
{
return uri;
}
public String getAction()
{
return action;
}
public List<String> getQueryParameters()
{
return Collections.unmodifiableList(queryParameters);
}
public Map<String, Object> getRequestData()
{
return Collections.unmodifiableMap(requestData);
}
#XmlElement
private String uri;
#XmlElement
private String action;
#XmlElementWrapper
private List<String> queryParameters = new ArrayList<String>();
#XmlPath(".")
#XmlJavaTypeAdapter(StringObjectMapAdapter.class)
private Map<String, Object> requestData = new HashMap<String, Object>();
}
The XmlAdpater:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlValue;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import org.eclipse.persistence.oxm.annotations.XmlVariableNode;
public class StringObjectMapAdapter extends
XmlAdapter<StringObjectMapAdapter.AdaptedMap, Map<String, Object>>
{
public static class AdaptedEntry
{
#XmlTransient
public String key;
#XmlValue
public Object value = new Object();
}
public static class AdaptedMap
{
#XmlVariableNode("key")
List<AdaptedEntry> entries = new ArrayList<AdaptedEntry>();
}
#Override
public AdaptedMap marshal(Map<String, Object> map) throws Exception
{
AdaptedMap adaptedMap = new AdaptedMap();
for (Entry<String, Object> entry : map.entrySet())
{
AdaptedEntry adaptedEntry = new AdaptedEntry();
adaptedEntry.key = entry.getKey();
adaptedEntry.value = entry.getValue();
adaptedMap.entries.add(adaptedEntry);
}
return adaptedMap;
}
#Override
public Map<String, Object> unmarshal(AdaptedMap adaptedMap) throws Exception
{
List<AdaptedEntry> adapatedEntries = adaptedMap.entries;
Map<String, Object> map = new HashMap<String, Object>(adapatedEntries.size());
for (AdaptedEntry adaptedEntry : adapatedEntries )
{
map.put(adaptedEntry.key, adaptedEntry.value);
}
return map;
}
}
and finally is my test app:
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.testng.annotations.Test;
public class TestStringObjectMapAdapter {
#Test
public void testUnmarshalFromJson() throws Exception
{
JAXBContext jc = JAXBContext.newInstance(CustomerRequest.class);
Unmarshaller unmarshaller = jc.createUnmarshaller();
unmarshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
unmarshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
StreamSource json = new StreamSource("test-data.json");
CustomerRequest request= unmarshaller.unmarshal(json,
CustomerRequest.class).getValue();
assert(request.getUri().equals("\\foo\\dosomthing"));
assert(request.getAction().equals("POST"));
}
}
Then when test app runs, an java.lang.ClassCastException exception is generated:
FAILED: testUnmarshalFromJson
java.lang.ClassCastException: com.sun.org.apache.xerces.internal.dom.DocumentImpl cannot be cast to org.w3c.dom.Element
at org.eclipse.persistence.internal.oxm.XMLCompositeObjectMappingNodeValue.endSelfNodeValue(XMLCompositeObjectMappingNodeValue.java:468)
at org.eclipse.persistence.internal.oxm.record.UnmarshalRecordImpl.endDocument(UnmarshalRecordImpl.java:606)
at org.eclipse.persistence.internal.oxm.record.UnmarshalRecordImpl.endElement(UnmarshalRecordImpl.java:1084)
at org.eclipse.persistence.internal.oxm.record.json.JSONReader.parse(JSONReader.java:304)
at org.eclipse.persistence.internal.oxm.record.json.JSONReader.parseRoot(JSONReader.java:179)
at org.eclipse.persistence.internal.oxm.record.json.JSONReader.parse(JSONReader.java:125)
at org.eclipse.persistence.internal.oxm.record.json.JSONReader.parse(JSONReader.java:140)
at org.eclipse.persistence.internal.oxm.record.SAXUnmarshaller.unmarshal(SAXUnmarshaller.java:857)
at org.eclipse.persistence.internal.oxm.record.SAXUnmarshaller.unmarshal(SAXUnmarshaller.java:707)
at org.eclipse.persistence.oxm.XMLUnmarshaller.unmarshal(XMLUnmarshaller.java:655)
at org.eclipse.persistence.jaxb.JAXBUnmarshaller.unmarshal(JAXBUnmarshaller.java:301)
at com.absolute.asb.urp.services.domain.TestStringObjectMapAdapter.testUnmarshalFromJson(TestStringObjectMapAdapter.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
Maybe you should try creating the correct MoXY JAXBContext like:
private static synchronized JAXBContext createJAXBContext() throws JAXBException {
if(jc == null){
jc = org.eclipse.persistence.jaxb.JAXBContextFactory.createContext(new Class[] {CustomerReqeust.class}, null);
}
return jc;
}
Or use another way like mentioned in http://blog.bdoughan.com/2011/05/specifying-eclipselink-moxy-as-your.html
Btw "CustomerReqeust" is a little bit wrong spelled :-)