Is it possible to make Jackson serialize a nested object as a string
It can be done with custom serializer:
class EscapedJsonSerializer extends StdSerializer<Object> { public EscapedJsonSerializer() { super((Class<Object>) null); } @Override public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { StringWriter str = new StringWriter(); JsonGenerator tempGen = new JsonFactory().setCodec(gen.getCodec()).createGenerator(str); if (value instanceof Collection || value.getClass().isArray()) { tempGen.writeStartArray(); if (value instanceof Collection) { for (Object it : (Collection) value) { writeTree(gen, it, tempGen); } } else if (value.getClass().isArray()) { for (Object it : (Object[]) value) { writeTree(gen, it, tempGen); } } tempGen.writeEndArray(); } else { provider.defaultSerializeValue(value, tempGen); } tempGen.flush(); gen.writeString(str.toString()); } @Override public void serializeWithType(Object value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException { StringWriter str = new StringWriter(); JsonGenerator tempGen = new JsonFactory().setCodec(gen.getCodec()).createGenerator(str); writeTree(gen, value, tempGen); tempGen.flush(); gen.writeString(str.toString()); } private void writeTree(JsonGenerator gen, Object it, JsonGenerator tempGen) throws IOException { ObjectNode tree = ((ObjectMapper) gen.getCodec()).valueToTree(it); tree.set("@class", new TextNode(it.getClass().getName())); tempGen.writeTree(tree); }}
and deserializer:
class EscapedJsonDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer { private final Map<JavaType, JsonDeserializer<Object>> cachedDeserializers = new HashMap<>(); @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { throw new IllegalArgumentException("EscapedJsonDeserializer should delegate deserialization for concrete class"); } @Override public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { JavaType type = (ctxt.getContextualType() != null) ? ctxt.getContextualType() : property.getMember().getType(); return cachedDeserializers.computeIfAbsent(type, (a) -> new InnerDeserializer(type)); } private class InnerDeserializer extends JsonDeserializer<Object> { private final JavaType javaType; private InnerDeserializer(JavaType javaType) { this.javaType = javaType; } @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String string = p.readValueAs(String.class); return ((ObjectMapper) p.getCodec()).readValue(string, javaType); } @Override public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { String str = p.readValueAs(String.class); TreeNode root = ((ObjectMapper) p.getCodec()).readTree(str); Class clz; try { clz = Class.forName(((TextNode) root.get("@class")).asText()); Object newJsonNode = p.getCodec().treeToValue(root, clz); return newJsonNode; } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } }}
The field should be annotated with @JsonSerialize and @JsonDeserialize (if needed)
class Outer { @JsonTypeInfo(include = JsonTypeInfo.As.PROPERTY, use = JsonTypeInfo.Id.CLASS) @JsonSerialize(using = EscapedJsonSerializer.class) @JsonDeserialize(using = EscapedJsonDeserializer.class) public Foo val;}
It works well with simple collections (list, arrays) and to some extent with polymorphism, although more elaborate solution may be needed for specific polymorphism related issues.Example output looks like this:
{"val":"{\"foo\":\"foo\",\"@class\":\"org.test.Foo\"}"}{"val":"{\"foo\":\"foo\",\"bar\":\"bar\",\"@class\":\"org.test.Bar\"}"}
I also couldn't find built-in solution and ended up writing custom converter:
public class ObjectToJsonStringConverter extends StdConverter<Object, String> { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public String convert(Object value) { try { return objectMapper.writeValueAsString(value); } catch (JsonProcessingException e) { throw new IllegalStateException(e); } }}
usage:
@Valueprivate static class Message { private final String type; @JsonSerialize(converter = ObjectToJsonStringConverter.class) private final MyType message;}