How can I configure Jackson to serialize Longs as Strings, but not Integers or doubles?

Viewed 6514

We were previously using a framework (Cloud Endpoints) that automatically serialized java.lang.Long as JSON Strings (while leaving Integers and Doubles as JSON numbers). We are migrating to use the more standard Jackson serialization framework.

To be backwards compatible, we need the ability for Jackson to serialize Longs as Strings, while keeping Integers and Doubles as JSON numbers by default. I don't see any feature in Jackson to do this. Is this possible?

2 Answers

Use custom serialisers for Long.class and long.class:

public static class LongSerializer extends JsonSerializer<Long> {
    @Override
    public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(value.toString());
    }
}

public static class LongDeserializer extends JsonDeserializer<Long> {
    @Override
    public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return p.getValueAsLong();
    }
}

You'll need to register them with your mapper:

public static void main(String[] args) throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();

    SimpleModule module = new SimpleModule();
    module.addSerializer(long.class, new LongSerializer());
    module.addSerializer(Long.class, new LongSerializer());
    module.addDeserializer(long.class, new LongDeserializer());
    module.addDeserializer(Long.class, new LongDeserializer());
    objectMapper.registerModule(module);

    Test test = new Test();
    String json = objectMapper.writeValueAsString(test);
    System.out.println(json);
}

Option 1. Custom serializer

@teppic answer should work for most of the cases, but

  1. You don't need custom deserializer. The default deserializer can handle String => long

  2. Can use com.fasterxml.jackson.databind.ser.std.ToStringSerializer serializer

    module.addSerializer(Long.class, ToStringSerializer.instance);
    module.addSerializer(long.class, ToStringSerializer.instance);
    
  3. You will get edge cases, because custom serializer approach actually can't handle cases when another custom serializer uses this JsonGenerator method

    (JsonGenerator) g.writeNumber((long)value);
    

    And the most important edge case here is long[].class

    static class MyBean {
        long val = 1;        // works fine "1"
        Long val2 = 2L;      // works fine "2"
        long[] vals = {1,2}; // doesn't work [1,2]
    }
    

So you actually can

public static void main(String[] args) throws IOException {
    ObjectMapper mapper = new ObjectMapper();
    SimpleModule module = new SimpleModule();
    module.addSerializer(Long.class, ToStringSerializer.instance);
    module.addSerializer(long.class, ToStringSerializer.instance);
    module.addSerializer(long[].class, new JsonSerializer<long[]>(){
        @Override
        public final void serialize(
                long[] value, JsonGenerator g, SerializerProvider provider)
                throws IOException {

            g.writeStartArray();
            for (int i = 0, len = value.length; i < len; ++i) 
                g.writeString(String.valueOf(value[i]));

            g.writeEndArray();
        }
    });

    // register every other serializers that use 
    // (JsonGenerator) g.writeNumber((long)value); 
    // like AtomicLong, if you use it in your code

    mapper.registerModule(module);

    MyBean bean = new MyBean();
    String json = mapper.writeValueAsString(bean);
    MyBean myBean2 = mapper.readValue(json, MyBean.class);

    System.out.println(json);
    System.out.println(myBean2);
    System.out.println(mapper.writeValueAsString(myBean2));
}

Option 2. Custom JsonFactory and JsonGenerator

But then again, is it enough for you and are you sure nobody will ever try to use jsonGenerator.writeNumber(long) or do you have some other class, that is actually using it by default? So the 100% working solution would be to register custom JsonFactory on ObjectMapper with 2 overridden methods _createGenerator and _createUTF8Generator

JsonFactory factory =  new JsonFactory(){
    @Override
    protected JsonGenerator _createGenerator(Writer out, IOContext ctxt)
            throws IOException {
        WriterBasedJsonGenerator gen = new WriterBasedJsonGenerator(ctxt,
                _generatorFeatures, _objectCodec, out){
            @Override
            public void writeNumber(long l) throws IOException {
                writeString(String.valueOf(l));
            }
        };
        return _internalSuperSetup(gen);
    }

    @Override
    protected JsonGenerator _createUTF8Generator(OutputStream out, IOContext ctxt)
            throws IOException {
        UTF8JsonGenerator gen = new UTF8JsonGenerator(ctxt,
                _generatorFeatures, _objectCodec, out){
            @Override
            public void writeNumber(long l) throws IOException {
                writeString(String.valueOf(l));
            }
        };
        return _internalSuperSetup(gen);
    }

    // reuse code from _createUTF8Generator and _createGenerator super methods
    private JsonGenerator _internalSuperSetup(JsonGenerator gen){
        if (_characterEscapes != null) gen.setCharacterEscapes(_characterEscapes);
        SerializableString rootSep = _rootValueSeparator;
        if (rootSep != DefaultPrettyPrinter.DEFAULT_ROOT_VALUE_SEPARATOR) 
            gen.setRootValueSeparator(rootSep);
        return gen;
    }
};
ObjectMapper mapper = new ObjectMapper(factory);
Related