Is it possible to make Jackson serialize a nested object as a string

a 夏天 提交于 2021-02-20 09:07:11

问题


Given these classes:

@Value
private static class Message {
    private final String type;
    private final MyType message;
}

@Value
public class MyType {
    private final String foo;
}

Jackson will produce:

{
  "Type" : "Test",
  "Message" : {"foo" : "bar"}
}

Is there some type of annotation or instruction I can give to Jackson to ask it to serialize the nested complex type as a string, e.g. the desired JSON would be:

{
  "Type" : "Test",
  "Message" : "{\"foo\" : \"bar\"}"
}

I tried both of these annotations on the message field:

 @JsonFormat(shape = JsonFormat.Shape.STRING)
 @JsonSerialize(as=String.class)

Neither has the desired impact. For now my "hack" is to do this at construction time:

return new Message("Test", mapper.writeValueAsString(new MyType("bar")));

I guess I could write a custom serializer, but I wondered if this is some type of standard behaviour that is built in. My use case is that I'm constructing a JSON payload which is expected to have a string message contained within it that itself contains JSON.

Environment

Jackson version is 2.9.0 using Spring Boot 2 on Java 10.


回答1:


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\"}"}



回答2:


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:

@Value
private static class Message {
    private final String type;

    @JsonSerialize(converter = ObjectToJsonStringConverter.class)
    private final MyType message;
}


来源:https://stackoverflow.com/questions/51141480/is-it-possible-to-make-jackson-serialize-a-nested-object-as-a-string

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!