I decided to take a crack at this, though it turned out to be more complicated than I would expect. With this solution, simply register the rawJsonModule
with your ObjectMapper
and then apply @RawJson
to the target field.
The code can definitely be optimized a bit too (the reflection in the deserializer is definitely suboptimal). Let me know if you have any questions.
Output: TestModel{test=123, testStr='foo', testSubModel=TestSubModel{testStr2='foobar', testFloat2=1.2, rawJson='{"testStr2":"foobar","testFloat2":1.2}'}}
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
public class SO67140419 {
public static void main(String[] args) throws JsonProcessingException {
String json = """
{
"test": 123,
"testStr": "foo",
"testSubModel": {
"testStr2": "foobar",
"testFloat2": 1.2
}
}""";
var rawJsonModule = new SimpleModule();
// Credits to schummar for this technique; take the default deserializer and
// pass it to RawJsonDeserializer, which intercepts all deserialization
// https://stackoverflow.com/a/18405958/5378187
rawJsonModule.setDeserializerModifier(new BeanDeserializerModifier() {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
return new RawJsonDeserializer(beanDesc, deserializer);
}
});
var mapper = new ObjectMapper();
mapper.registerModule(rawJsonModule);
var model = mapper.readValue(json, TestModel.class);
System.out.println(model);
}
}
@JsonIgnore
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface RawJson {}
class RawJsonDeserializer extends StdDeserializer<Object> implements ResolvableDeserializer {
/**
* The default deserializer
*/
private final JsonDeserializer<?> deser;
private final BeanDescription desc;
public RawJsonDeserializer(BeanDescription desc, JsonDeserializer<?> deser) {
super((JavaType) null);
this.deser = deser;
this.desc = desc;
}
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// Read p into a json node in case we need it later for an @RawJson field
JsonNode node = p.getCodec().readTree(p);
p = p.getCodec().treeAsTokens(node); // Refresh p
if (p.getCurrentToken() == null) {
p.nextToken();
}
// Deserialize object using the default deserialization
Object obj = deser.deserialize(p, ctxt);
// Check for RawJson annotated fields
for (Field f : desc.getBeanClass().getDeclaredFields()) {
if (f.getDeclaredAnnotation(RawJson.class) == null) {
continue;
} else if (f.getType() != String.class) {
throw new IllegalStateException("@RawJson annotation applied to non-string field: " + f);
}
// Set the field to the json we stored earlier
try {
f.setAccessible(true);
f.set(obj, node.toString());
} catch (IllegalAccessException e) {
throw new IOException(e);
}
}
return obj;
}
@Override
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
// Not sure why we need this but ok
if (deser instanceof ResolvableDeserializer rd) {
rd.resolve(ctxt);
}
}
}
class TestModel {
private int test;
private String testStr;
private TestSubModel testSubModel;
public int getTest() {
return test;
}
public String getTestStr() {
return testStr;
}
public TestSubModel getTestSubModel() {
return testSubModel;
}
@Override
public String toString() {
return "TestModel{" +
"test=" + test +
", testStr='" + testStr + '\'' +
", testSubModel=" + testSubModel +
'}';
}
}
class TestSubModel {
private String testStr2;
private float testFloat2;
@RawJson
private String rawJson; // i want this to contain something like { "testStr2": "foobar", "testFloat2": 1.2 }
public String getTestStr2() {
return testStr2;
}
public float getTestFloat2() {
return testFloat2;
}
@Override
public String toString() {
return "TestSubModel{" +
"testStr2='" + testStr2 + '\'' +
", testFloat2=" + testFloat2 +
", rawJson='" + rawJson + '\'' +
'}';
}
}