I have a large JSON file that is made up of structures that I am mapping into POJOs, and then storing in a Collection. The structure is similar to this:
[
{
"id": 1234,
"file": "C:\\Programs\\Program1.exe",
"exists": true
}
{
"id": 5678,
"file": "C:\\Programs\\Program2.exe",
"exists": false
}
...
]
Using the Jackson streaming API I have got all these structures read, and the POJOs stored in a Collection successfully. My POJO class looks like this:
public class Programs
{
#JsonProperty("id")
private Integer id;
#JsonProperty("file")
private String file;
#JsonProperty("exists")
private Boolean exists;
#JsonGetter("id")
public Integer getId()
{
return id;
}
#JsonGetter("file")
public String getFile()
{
return file;
}
#JsonGetter("exists")
public Boolean getExists()
{
return exists;
}
}
However, I want to omit any structures that have "exists" set to false during the deserialization process so that no POJO is ever created for them. So I wrote a custom deserializer with the help of this SO question [ How do I call the default deserializer from a custom deserializer in Jackson ], with my overridden deserialize looking like:
#Override
public Programs deserialize(JsonParser parser, DeserializationContext context)
throws IOException
{
Programs programs = (Programs)defaultDeserializer.deserialize(parser, context);
if (!programs.getExists())
{
throw context.mappingException("[exists] value is false.");
}
return programs;
}
However, when I run some unit tests, I get the following error:
"Can not deserialize instance of java.util.ArrayList out of START_OBJECT token"
message was "Class com.myprogram.serializer.ProgramsJsonDeserializer
has no default (no arg) constructor"
(Adding a no arg constructor gives the error that StdDeserializer does not have a default constructor.)
Is this the correct approach to achieving what I am trying to do? And does anyone know why I get this error message?
I want to omit any structures that have "exists" set to false during
the deserialization process so that no POJO is ever created for them.
I think your objective is to retrieve a list of Programs instance that only have exists set to true after derserialization. A customized CollectionDeserializer to filter those unwanted instance may help:
public class ProgramsCollectionHandler extends SimpleModule {
private static class ProgramsCollectionDeserializer extends CollectionDeserializer {
public ProgramsCollectionDeserializer(CollectionDeserializer deserializer) {
super(deserializer);
}
private static final long serialVersionUID = 1L;
#Override
public Collection<Object> deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
Collection<Object> result = super.deserialize(parser, context);
Collection<Object> filteredResult = new ArrayList<Object>();
for (Object o : result) {
if (o instanceof Programs) {
final Programs programs = (Programs) o;
if (programs.exists) {
filteredResult.add(programs);
}
}
}
return filteredResult;
}
#Override
public CollectionDeserializer createContextual(
DeserializationContext context,
BeanProperty property) throws JsonMappingException {
return new ProgramsCollectionDeserializer(super.createContextual(context, property));
}
}
private static final long serialVersionUID = 1L;
#Override
public void setupModule(Module.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 ProgramsCollectionDeserializer(
(CollectionDeserializer) deserializer);
}
return super.modifyCollectionDeserializer(config, type,
beanDesc, deserializer);
}
});
}
}
After that, your can register it into your object mapper:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ProgramsCollectionHandler());
"Can not deserialize instance of java.util.ArrayList out of
START_OBJECT token" message was "Class
com.myprogram.serializer.ProgramsJsonDeserializer has no default (no
arg) constructor"
(Adding a no arg constructor gives the error that StdDeserializer does not have a default constructor.)
This may be because your constructor cannot be accessed. For example, your deserializer is implemented as a non-static inner class.
Related
I'd like to deserialize an object from YAML with the following properties, using Jackson in a Spring Boot application:
Abstract class Vehicle, implemented by Boat and Car
For simplicity, imagine both have a name, but only Boat has also a seaworthy property, while Car has a top-speed.
mode-of-transport:
type: boat
name: 'SS Boatface'
seaworthy: true
----
mode-of-transport:
type: car`
name: 'KITT'
top-speed: 123
This all works fine in my annotated subclasses using #JsonTypeInfo and #JsonSubTypes!
Now, I'd like to create a shorthand using only a String value, which should create a Car by default with that name:
mode-of-transport: 'KITT'
I tried creating my own custom serializer, but got stuck on most of the relevant details. Please help me fill this in, if this is the right approach:
public class VehicleDeserializer extends StdDeserializer<Merger> {
/* Constructors here */
#Override
public Vehicle deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (/* it is an OBJECT */){
// Use the default polymorphic deserializer
} else if (/* it is a STRING */) {
Car car = new Car();
car.setName( /* the String value */ );
return car;
}
return ???; /* what to return here? */
}
}
I found these 2 answers for inspiration, but it looks like combining it with polymorphic types makes it more difficult: How do I call the default deserializer from a custom deserializer in Jackson and Deserialize to String or Object using Jackson
A few things are different than the solutions offered in those questions:
I am processing YAML, not JSON. Not sure about the subtle differences there.
I have no problem hardcoding the 'default' type for Strings inside my Deserializer, hopefully making it simpler.
This was actually easier than I thought to solve it. I got it working using the following:
Custom deserializer implementation:
public class VehicleDeserializer extends StdDeserializer<Vehicle> {
public VehicleDeserializer() {
super(Vehicle.class);
}
#Override
public Vehicle deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.currentToken() == JsonToken.VALUE_STRING) {
Car car = new Car();
car.setName(jp.readValueAs(String.class));
return car;
}
return jp.readValueAs(Vehicle.class);
}
}
To avoid circular dependencies and to make the custom deserializer work with the polymorphic #JsonTypeInfo and #JsonSubTypes annotations I kept those annotations on the class level of Vehicle, but put the following annotations on the container object I am deserializing:
public class Transport {
#JsonDeserialize(using = VehicleDeserializer.class)
#JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
private Vehicle modeOfTransport;
// Getter, setters
}
This means that by default a Vehicle is deserialized as a polymorphic object, unless explicitly specified to deserialize it using my custom deserializer. This deserializer will then in turn defer to the polymorphism if the input is not a String.
Hopefully this will help someone running into this issue :)
So there is a solution that requires you to handle the jackson errors using a DeserializationProblemHandler (since you want to parse the same type using different inputs, this is not achieved easily using regular means):
public class MyTest {
#Test
public void doTest() throws JsonParseException, JsonMappingException, IOException {
final ObjectMapper om = new ObjectMapper();
om.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(final DeserializationContext ctxt, final Class<?> instClass, final JsonParser p, final String msg) throws IOException {
if (instClass.equals(Car.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return new Car();
}
}
return NOT_HANDLED;
}
#Override
public JavaType handleMissingTypeId(final DeserializationContext ctxt, final JavaType baseType, final TypeIdResolver idResolver, final String failureMsg) throws IOException {
// if (baseType.isTypeOrSubTypeOf(Vehicle.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return TypeFactory.defaultInstance().constructType(Car.class);
}
return super.handleMissingTypeId(ctxt, baseType, idResolver, failureMsg);
}
});
final Container objectValue = om.readValue(getObjectJson(), Container.class);
assertTrue(objectValue.getModeOfTransport() instanceof Car);
final Container stringValue = om.readValue(getStringJson(), Container.class);
assertTrue(stringValue.getModeOfTransport() instanceof Car);
}
private String getObjectJson() {
return "{ \"modeOfTransport\": { \"type\": \"car\", \"name\": \"KITT\", \"speed\": 1}}";
}
private String getStringJson() {
return "{ \"modeOfTransport\": \"KITT\"}";
}
}
class Container {
private Vehicle modeOfTransport;
public Vehicle getModeOfTransport() {
return modeOfTransport;
}
public void setModeOfTransport(final Vehicle modeOfTransport) {
this.modeOfTransport = modeOfTransport;
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
#JsonSubTypes({
#Type(name = "car", value = Car.class)
})
abstract class Vehicle {
protected String type;
protected String name;
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
}
#JsonTypeName("car")
class Car extends Vehicle {
private int speed;
public int getSpeed() {
return speed;
}
public void setSpeed(final int speed) {
this.speed = speed;
}
}
Note that I used JSON, not YAML, and you need to add your other subtypes as well.
I want to write json deserializer on class Type so that when Type is deserialized from given json based on name it maps value (of type interface Being) to its current implementation based on some factory method that returns correct class name based on name, and populates remaining class without any explicit deserialization and without creating object of TigerBeing or HumanBeing explicitly using new.
I tried to use #jsonCreator but there i have to initialize entire HumanBeing or TigerBeing using new and passing all json in constructor. I need auto mapping for types further used as further pojo can be quite complex.
{type:[{
"name": "Human",
"value": {
"height":6,
"weight":100,
"languages":["spanish","english"]
}
},
{
"name":"Tiger",
"value":{
"extinct":1,
"found":["Asia", "America", "Europe", "Africa"]
}
}
]}
I have:
public class Type {
String name;
Being value;
}
public interface Being {
}
public class TigerBeing implements Being {
Integer extinct;
String[] found;
}
public class HumanBeing implement Being {
Integer height;
Integer weight;
String[] languages;
}
import java.io.IOException;
public class BeingDeserializer extends JsonDeserializer<Being> {
#Override
public Expertise deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonMappingException {
JsonNode node = jp.getCodec().readTree(jp);
String beingName = node.get("name").asText();
JsonNode valueNode = node.get("value");
BeingName beingByName = ExpertiseName.getBeingByName(beingName);
if(beingByName ==null) {
throw new JsonMappingException("Invalid Being " + beingName);
}
Being being = JsonUtils.getObjectFromJsonNode(valueNode,
ExpertiseFactory.getExpertise(beingByName).getClass());
return being;
}
}
In this way I was able to solve the above problem.
I have an object in which one of the properties is a Map<MyEnum, Object>.
As my application is quite big, I've enabled default typing as so :
ObjectMapper jsonMapper = new ObjectMapper()
.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_OBJECT)
.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
This is rather good, generally speaking.
But, as Javascript doesn't support object keys when using objects as hashes, when I put some data in that map from the javascript side, the object is transformed into a string.
As a consequence, the JSON I receive contains
"MyClass": {
"contextElements": {
"userCredentials": {
"UserCredentials": {
"login": "admin",
"password": "admin",
}
}
}
},
When deserializing that, Jackson fails with the following exception
java.lang.IllegalArgumentException: Invalid type id 'userCredentials' (for id type 'Id.class'): no such class found
at org.codehaus.jackson.map.jsontype.impl.ClassNameIdResolver.typeFromId(ClassNameIdResolver.java:72)
at org.codehaus.jackson.map.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:61)
at org.codehaus.jackson.map.jsontype.impl.AsWrapperTypeDeserializer._deserialize(AsWrapperTypeDeserializer.java:87)
at org.codehaus.jackson.map.jsontype.impl.AsWrapperTypeDeserializer.deserializeTypedFromObject(AsWrapperTypeDeserializer.java:39)
at org.codehaus.jackson.map.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:133)
at org.codehaus.jackson.map.deser.SettableBeanProperty$MethodProperty.deserializeAndSet(SettableBeanProperty.java:221)
Which I quite well understand : Jackson doesn't understand the Map<MyEnum, Object> declaration in my class and, although MyEnum is a final class, wants some type metadata added (hey, maybe it's a bug ?!).
What can I do to ahve that code working ?
I'm using Jackson 1.5.2
OK, so, the question states it correctly : it is not possible to use JSON maps in which keys are not strings. As a consequence, to emulate the Java Map in javascript, one has to go a longer path, which would typically involve transforming the map into ... something else.
What I chose was the quite usual array of arrays :
a map such as
{
a:b,
c:d,
}
Will then be translated into the array
[
[a,b],
[c,d],
]
What are the detailled steps required to obtain that result
Configure custom (de)serialization
This is obtained by setting a serialization factory into the object mapper, as Jackson doc clearly explains :
/**
* Associates all maps with our custom serialization mechanism, which will transform them into arrays of arrays
* #see MapAsArraySerializer
* #return
*/
#Produces
public SerializerFactory createSerializerFactory() {
CustomSerializerFactory customized = new CustomSerializerFactory();
customized.addGenericMapping(Map.class, new MapAsArraySerializer());
return customized;
}
public #Produces ObjectMapper createMapper() {
ObjectMapper jsonMapper = new ObjectMapper();
// ....
// now configure serializer
jsonMapper.setSerializerFactory(createSerializerFactory());
// ....
return jsonMapper;
}
The process seems quite simple, mainly because serialization provides quite correct polymorphism features in serialization, which are not that good for deserialization. Indeed, as doc also states, deserialization requires adding explicit class mappings, which are not used in any object-oriented fashion (inheritence is not supported there)
/**
* Defines a deserializer for each and any used map class, as there is no inheritence support ind eserialization
* #return
*/
#Produces
public DeserializerProvider createDeserializationProvider() {
// Yeah it's not even a standard Jackson class, it'll be explained why later
CustomDeserializerFactory factory = new MapAsArrayDeserializerFactory();
List<Class<? extends Map>> classesToHandle = new LinkedList<>();
classesToHandle.add(HashMap.class);
classesToHandle.add(LinkedHashMap.class);
classesToHandle.add(TreeMap.class);
for(Class<? extends Map> c : classesToHandle) {
addClassMappingFor(c, c, factory);
}
// and don't forget interfaces !
addClassMappingFor(Map.class, HashMap.class, factory);
addClassMappingFor(SortedMap.class, TreeMap.class, factory);
return new StdDeserializerProvider(factory);
}
private void addClassMappingFor(final Class<? extends Map> detected, final Class<? extends Map> created, CustomDeserializerFactory factory) {
factory.addSpecificMapping(detected, new MapAsArrayDeserializer() {
#Override
protected Map createNewMap() throws Exception {
return created.newInstance();
}
});
}
// It's the same createMapper() method that was described upper
public #Produces ObjectMapper createMapper() {
ObjectMapper jsonMapper = new ObjectMapper();
// ....
// and deserializer
jsonMapper.setDeserializerProvider(createDeserializationProvider());
return jsonMapper;
}
Now we have correctly defined how (de)serialization is customized, or do we have ? In fact, no : the MapAsArrayDeserializerFactory deserves its own explanation.
After some debugging, I found that DeserializerProvider delegates to the DeserializerFactory when no deserializer exists for class, which is cool. But, the DeserializerFactory creates the deserializer according to the "kind" of obejct : if it is a collection, then a CollectionDeserializer will be used (to read the array into a Collection). If it's a Map, then the MapDeserializer will be used.
Unfortunatly, this resolution uses the java class given in the JSON stream (especially when using polymorphic deserialization, which was my case). As a consequence, configuring custom deserialization has no effect, unless the CustomDeserializerFactory is customized ... like that :
public class MapAsArrayDeserializerFactory extends CustomDeserializerFactory {
#Override
public JsonDeserializer<?> createMapDeserializer(DeserializationConfig config, MapType type, DeserializerProvider p) throws JsonMappingException {
return createBeanDeserializer(config, type, p);
}
}
Yup, i deserialize all maps as beans. But now, all my deserializers are correctly called.
Serializing
Now, serialization is a rather simple task :
public class MapAsArraySerializer extends JsonSerializer<Map> {
#SuppressWarnings("unchecked")
private Set asListOfLists(Map<?, ?> value) {
Set returned = new HashSet<>();
for(Map.Entry e : value.entrySet()) {
returned.add(Arrays.asList(e.getKey(), e.getValue()));
}
return returned;
}
#Override
public void serialize(Map value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
Collection entries = asListOfLists(value);
jgen.writeObjectField("entries", entries);
}
#Override
public void serializeWithType(Map value, JsonGenerator jgen, SerializerProvider provider, TypeSerializer typeSer) throws IOException,
JsonProcessingException {
Collection entries = asListOfLists(value);
typeSer.writeTypePrefixForObject(value, jgen);
jgen.writeObjectField("entries", entries);
typeSer.writeTypeSuffixForObject(value, jgen);
}
}
Deserialization
And deserialization is not more complex :
public abstract class MapAsArrayDeserializer<Type extends Map> extends JsonDeserializer<Type> {
protected Type newMap(Collection c, Type returned) {
for(Object o : c) {
if (o instanceof List) {
List l = (List) o;
if(l.size()==2) {
Iterator i = l.iterator();
returned.put(i.next(), i.next());
}
}
}
return returned;
}
protected abstract Type createNewMap() throws Exception;
#Override
public Type deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
if(jp.getCurrentToken().equals(JsonToken.START_OBJECT)) {
JsonToken nameToken = jp.nextToken();
String name = jp.getCurrentName();
if(name.equals("entries")) {
jp.nextToken();
Collection entries = jp.readValueAs(Collection.class);
JsonToken endMap = jp.nextToken();
try {
return newMap(entries, createNewMap());
} catch(Exception e) {
throw new IOException("unable to create receiver map", e);
}
} else {
throw new IOException("expected \"entries\", but field name was \""+name+"\"");
}
} else {
throw new IOException("not startying an object ? Not possible");
}
}
#Override
public Type deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException,
JsonProcessingException {
Object value = typeDeserializer.deserializeTypedFromObject(jp, ctxt);
return (Type) value;
}
}
Well, expected the class is left abstract t have each declared subtype create the right map instance.
And now
And now it works seamlessly on Java side (cause the Javascript has to have a map-equivalent object to read those datas.
I want to have Jackson always parse numbers as Long or Double.
I have a class like the following with the corresponding getters and setters:
public class Foo {
private HashMap<String, ArrayList<HashMap<String, Object>>> tables;
...
}
And some Json that looks like so:
{ "tables" :
{ "table1" :
[
{ "t1Field1" : 0,
"t1Field2" : "val2"
},
{ "t1Field1" : 1,
"t1Field2" : "val4"
}
]
}
}
Jackson will parse the values for t1Field1 as Integers/Longs and Floats/Doubles based on the size of the number. But I want to always get Longs and Doubles.
I'm almost certain I have to write a custom deserializer or parser to do this and I have looked through examples but haven't found anything that works how I would imagine. I just want to extend existing Jackson functionality and override what happens for numbers. I don't want to write a whole deserializer for Objects. I just want to do something like:
public class CustomerNumberDeserializer extends SomethingFromCoreJackson {
public Object deserialize() {
Object num;
num = super.deserialize();
if (num instanceof Integer)
return Long.valueOf(((Integer)num).intValue());
return num;
}
}
But all the Jackson classes that I thought to extend were either final or abstract and seemed to require a bunch of extra work. Is what I want possible?
After revisiting this I found the class that I wanted to extend. Hope this helps someone.
I created a custom deserializer as follows:
/**
* Custom deserializer for untyped objects that ensures integers are returned as longs
*/
public class ObjectDeserializer extends UntypedObjectDeserializer {
private static final long serialVersionUID = 7764405880012867708L;
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
Object out = super.deserialize(jp, ctxt);
if (out instanceof Integer) {
return Long.valueOf((Integer)out).longValue();
}
return out;
}
#Override
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt,
TypeDeserializer typeDeserializer) throws IOException {
Object out = super.deserializeWithType(jp, ctxt, typeDeserializer);
if (out instanceof Integer) {
return Long.valueOf((Integer)out).longValue();
}
return out;
}
}
And configured my object mapper to use it:
ObjectMapper om = new ObjectMapper();
SimpleModule mod = new SimpleModule().addDeserializer(Object.class, new ObjectDeserializer());
om.registerModule(mod);
I am trying to include raw JSON inside a Java object when the object is (de)serialized using Jackson. In order to test this functionality, I wrote the following test:
public static class Pojo {
public String foo;
#JsonRawValue
public String bar;
}
#Test
public void test() throws JsonGenerationException, JsonMappingException, IOException {
String foo = "one";
String bar = "{\"A\":false}";
Pojo pojo = new Pojo();
pojo.foo = foo;
pojo.bar = bar;
String json = "{\"foo\":\"" + foo + "\",\"bar\":" + bar + "}";
ObjectMapper objectMapper = new ObjectMapper();
String output = objectMapper.writeValueAsString(pojo);
System.out.println(output);
assertEquals(json, output);
Pojo deserialized = objectMapper.readValue(output, Pojo.class);
assertEquals(foo, deserialized.foo);
assertEquals(bar, deserialized.bar);
}
The code outputs the following line:
{"foo":"one","bar":{"A":false}}
The JSON is exactly how I want things to look. Unfortunately, the code fails with an exception when attempting to read the JSON back in to the object. Here is the exception:
org.codehaus.jackson.map.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: java.io.StringReader#d70d7a; line: 1, column: 13] (through reference chain: com.tnal.prism.cobalt.gather.testing.Pojo["bar"])
Why does Jackson function just fine in one direction but fail when going the other direction? It seems like it should be able to take its own output as input again. I know what I'm trying to do is unorthodox (the general advice is to create an inner object for bar that has a property named A), but I don't want to interact with this JSON at all. My code is acting as a pass-through for this code -- I want to take in this JSON and send it back out again without touching a thing, because when the JSON changes I don't want my code to need modifications.
Thanks for the advice.
EDIT: Made Pojo a static class, which was causing a different error.
#JsonRawValue is intended for serialization-side only, since the reverse direction is a bit trickier to handle. In effect it was added to allow injecting pre-encoded content.
I guess it would be possible to add support for reverse, although that would be quite awkward: content will have to be parsed, and then re-written back to "raw" form, which may or may not be the same (since character quoting may differ).
This for general case. But perhaps it would make sense for some subset of problems.
But I think a work-around for your specific case would be to specify type as 'java.lang.Object', since this should work ok: for serialization, String will be output as is, and for deserialization, it will be deserialized as a Map. Actually you might want to have separate getter/setter if so; getter would return String for serialization (and needs #JsonRawValue); and setter would take either Map or Object. You could re-encode it to a String if that makes sense.
Following #StaxMan answer, I've made the following works like a charm:
public class Pojo {
Object json;
#JsonRawValue
public String getJson() {
// default raw value: null or "[]"
return json == null ? null : json.toString();
}
public void setJson(JsonNode node) {
this.json = node;
}
}
And, to be faithful to the initial question, here is the working test:
public class PojoTest {
ObjectMapper mapper = new ObjectMapper();
#Test
public void test() throws IOException {
Pojo pojo = new Pojo("{\"foo\":18}");
String output = mapper.writeValueAsString(pojo);
assertThat(output).isEqualTo("{\"json\":{\"foo\":18}}");
Pojo deserialized = mapper.readValue(output, Pojo.class);
assertThat(deserialized.json.toString()).isEqualTo("{\"foo\":18}");
// deserialized.json == {"foo":18}
}
}
I was able to do this with a custom deserializer (cut and pasted from here)
package etc;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
/**
* Keeps json value as json, does not try to deserialize it
* #author roytruelove
*
*/
public class KeepAsJsonDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
TreeNode tree = jp.getCodec().readTree(jp);
return tree.toString();
}
}
Use it by annotating the desired member like this:
#JsonDeserialize(using = KeepAsJsonDeserializer.class)
private String value;
#JsonSetter may help. See my sample ('data' is supposed to contain unparsed JSON):
class Purchase
{
String data;
#JsonProperty("signature")
String signature;
#JsonSetter("data")
void setData(JsonNode data)
{
this.data = data.toString();
}
}
This is a problem with your inner classes. The Pojo class is a non-static inner class of your test class, and Jackson cannot instantiate that class. So it can serialize, but not deserialize.
Redefine your class like this:
public static class Pojo {
public String foo;
#JsonRawValue
public String bar;
}
Note the addition of static
Adding to Roy Truelove's great answer, this is how to inject the custom deserialiser in response to appearance of #JsonRawValue:
import com.fasterxml.jackson.databind.Module;
#Component
public class ModuleImpl extends Module {
#Override
public void setupModule(SetupContext context) {
context.addBeanDeserializerModifier(new BeanDeserializerModifierImpl());
}
}
import java.util.Iterator;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
public class BeanDeserializerModifierImpl extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> it = builder.getProperties();
while (it.hasNext()) {
SettableBeanProperty p = it.next();
if (p.getAnnotation(JsonRawValue.class) != null) {
builder.addOrReplaceProperty(p.withValueDeserializer(KeepAsJsonDeserialzier.INSTANCE), true);
}
}
return builder;
}
}
This easy solution worked for me:
public class MyObject {
private Object rawJsonValue;
public Object getRawJsonValue() {
return rawJsonValue;
}
public void setRawJsonValue(Object rawJsonValue) {
this.rawJsonValue = rawJsonValue;
}
}
So I was able to store raw value of JSON in rawJsonValue variable and then it was no problem to deserialize it (as object) with other fields back to JSON and send via my REST. Using #JsonRawValue didnt helped me because stored JSON was deserialized as String, not as object, and that was not what I wanted.
This even works in a JPA entity:
private String json;
#JsonRawValue
public String getJson() {
return json;
}
public void setJson(final String json) {
this.json = json;
}
#JsonProperty(value = "json")
public void setJsonRaw(JsonNode jsonNode) {
// this leads to non-standard json, see discussion:
// setJson(jsonNode.toString());
StringWriter stringWriter = new StringWriter();
ObjectMapper objectMapper = new ObjectMapper();
JsonGenerator generator =
new JsonFactory(objectMapper).createGenerator(stringWriter);
generator.writeTree(n);
setJson(stringWriter.toString());
}
Ideally the ObjectMapper and even JsonFactory are from the context and are configured so as to handle your JSON correctly (standard or with non-standard values like 'Infinity' floats for example).
Here is a full working example of how to use Jackson modules to make #JsonRawValue work both ways (serialization and deserialization):
public class JsonRawValueDeserializerModule extends SimpleModule {
public JsonRawValueDeserializerModule() {
setDeserializerModifier(new JsonRawValueDeserializerModifier());
}
private static class JsonRawValueDeserializerModifier extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
builder.getProperties().forEachRemaining(property -> {
if (property.getAnnotation(JsonRawValue.class) != null) {
builder.addOrReplaceProperty(property.withValueDeserializer(JsonRawValueDeserializer.INSTANCE), true);
}
});
return builder;
}
}
private static class JsonRawValueDeserializer extends JsonDeserializer<String> {
private static final JsonDeserializer<String> INSTANCE = new JsonRawValueDeserializer();
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return p.readValueAsTree().toString();
}
}
}
Then you can register the module after creating the ObjectMapper:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());
String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);
I had the exact same issue.
I found the solution in this post :
Parse JSON tree to plain class using Jackson or its alternatives
Check out the last answer.
By defining a custom setter for the property that takes a JsonNode as parameter and calls the toString method on the jsonNode to set the String property, it all works out.
Using an object works fine both ways... This method has a bit of overhead deserializing the raw value in two times.
ObjectMapper mapper = new ObjectMapper();
RawJsonValue value = new RawJsonValue();
value.setRawValue(new RawHello(){{this.data = "universe...";}});
String json = mapper.writeValueAsString(value);
System.out.println(json);
RawJsonValue result = mapper.readValue(json, RawJsonValue.class);
json = mapper.writeValueAsString(result.getRawValue());
System.out.println(json);
RawHello hello = mapper.readValue(json, RawHello.class);
System.out.println(hello.data);
RawHello.java
public class RawHello {
public String data;
}
RawJsonValue.java
public class RawJsonValue {
private Object rawValue;
public Object getRawValue() {
return rawValue;
}
public void setRawValue(Object value) {
this.rawValue = value;
}
}
I had a similar problem, but using a list with a lot of JSON itens (List<String>).
public class Errors {
private Integer status;
private List<String> jsons;
}
I managed the serialization using the #JsonRawValue annotation. But for deserialization I had to create a custom deserializer based on Roy's suggestion.
public class Errors {
private Integer status;
#JsonRawValue
#JsonDeserialize(using = JsonListPassThroughDeserialzier.class)
private List<String> jsons;
}
Below you can see my "List" deserializer.
public class JsonListPassThroughDeserializer extends JsonDeserializer<List<String>> {
#Override
public List<String> deserialize(JsonParser jp, DeserializationContext cxt) throws IOException, JsonProcessingException {
if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
final List<String> list = new ArrayList<>();
while (jp.nextToken() != JsonToken.END_ARRAY) {
list.add(jp.getCodec().readTree(jp).toString());
}
return list;
}
throw cxt.instantiationException(List.class, "Expected Json list");
}
}