128

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.

skaffman
  • 398,947
  • 96
  • 818
  • 769
bhilstrom
  • 1,471
  • 2
  • 11
  • 8

12 Answers12

75

@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.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • 1
    This works like a charm; see my response for code (_formatting in the comments is awgul_). – yves amsellem Jul 12 '12 at 12:55
  • I had a different use case for this. Seems like if we don't want to generate a lot of string garbage in the deser/ser, we should be able to just passthrough a string as such. I saw a thread that tracked this, but it seems there is no native support possible. Have a look at http://markmail.org/message/qjncoohoalre4vd2#query:+page:1+mid:6ol54pw3zsr4jbhr+state:results – Sid Feb 10 '16 at 15:09
  • @Sid there is no way to do that AND tokenization both efficiently. To support pass-through of unprocessed tokens would require additional state-keeping, which makes "regular" parsing somewhat less efficient. It is sort of like optimization between regular code and exception throwing: to support latter adds overhead for former. Jackson has not been designed to try to keep unprocessed input available; it'd be nice to have it (for error messages, too) around, but would require different approach. – StaxMan Feb 12 '16 at 21:43
66

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}
  }
}
Community
  • 1
  • 1
yves amsellem
  • 7,106
  • 5
  • 44
  • 68
  • 1
    I didn't try, but it should work: 1) make a JsonNode node instead of Object json 2) use node.asText() instead of toString(). I am not sure about the 2nd one though. – Vadim Kirilchuk Aug 28 '15 at 16:09
  • I wonder why `getJson()` does return a `String` after all. If it just returned the `JsonNode` that was set through the setter, it would be serialized as desired, no? – sorrymissjackson Nov 11 '16 at 07:48
  • @VadimKirilchuk `node.asText()` returns empty value opposite `toString()`. – v.ladynev Mar 08 '18 at 11:04
56

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;
Adam Michalik
  • 9,678
  • 13
  • 71
  • 102
Roy Truelove
  • 22,016
  • 18
  • 111
  • 153
  • 8
    Amazing how simple. IMO this should be the official answer. I tried with a very complex structure containing arrays, subobjects, etc. Maybe you edit your answer and add that the String member to be deserialized should be annotated by @JsonDeserialize( using = KeepAsJsonDeserialzier.class ). (and correct the typo in your class name ;-) – Heri Sep 29 '15 at 11:46
  • this works for Deserializion. How about for Serialization of raw json into a pojo? How would that be accomplished – xtrakBandit Nov 13 '15 at 00:14
  • 5
    @xtrakBandit for Serialization, use `@JsonRawValue` – vr3C Jan 19 '16 at 13:07
  • This works like a charm. Thank you Roy and @Heri ..combination of this post together with Heri's comment is imho the best answer. – Michal Apr 21 '17 at 08:33
  • Simple and neat solution. I agree with @Heri – mahesh nanayakkara Aug 09 '18 at 07:18
  • It removes JsonTypeInfo.Id from json. –  Feb 08 '23 at 20:28
24

@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();
    }
}
anagaf
  • 2,120
  • 1
  • 18
  • 15
  • 3
    According to JsonNode.toString() documentation Method that will produce developer-readable representation of the node; which may or may not be as valid JSON. So this is actually very risky implementation. – Piotr Nov 20 '18 at 13:47
  • 1
    @Piotr the javadoc now says "Method that will produce (as of Jackson 2.10) valid JSON using default settings of databind, as String" – bernie Apr 28 '20 at 20:11
5

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

skaffman
  • 398,947
  • 96
  • 818
  • 769
  • Thanks. That got me one step further, but now I'm getting a different error. I'll update the original post with the new error. – bhilstrom Jan 24 '11 at 15:59
4

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;
    }
}
Community
  • 1
  • 1
Amir Abiri
  • 8,847
  • 11
  • 41
  • 57
  • this doesn't work in Jackson 2.9. Looks like it was broken as it's now uses old property in PropertyBasedCreator.construct instead of replacing one – dant3 May 23 '19 at 21:33
4

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.

4

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).

Georg
  • 987
  • 8
  • 16
  • 1
    According to `JsonNode.toString()` documentation `Method that will produce developer-readable representation of the node; which may or may not be as valid JSON.` So this is actually very risky implementation. – Piotr Nov 20 '18 at 13:46
  • Hi @Piotr , thanks for the hint. You're right of course, this uses the `JsonNode.asText()` internally and will output Infinity and other non-standard JSON values. – Georg Nov 27 '18 at 10:14
  • @Piotr the javadoc now says "Method that will produce (as of Jackson 2.10) valid JSON using default settings of databind, as String" – bernie Apr 28 '20 at 20:11
3

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);
Helder Pereira
  • 5,522
  • 2
  • 35
  • 52
  • Is there anything else in addition to the above that you have to do? I have found that the deserialize method of the JsonRawValueDeserializer never gets called by the ObjectMapper – Michael Coxon Aug 09 '18 at 21:52
  • @MichaelCoxon Did you manage to make it work? One thing that caused me issues in the past was using annotations from `org.codehaus.jackson` package without realising it. Make sure that all your imports come from `com.fasterxml.jackson`. – Helder Pereira Aug 10 '18 at 09:52
1

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.

Community
  • 1
  • 1
Alex Kubity
  • 364
  • 4
  • 16
1

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;
    }
}
Vozzie
  • 345
  • 2
  • 9
1

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");
    }
}
Biga
  • 621
  • 5
  • 7