Search code examples
javajsonjacksonbase64jackson-databind

Jackson serialize Object to JSON to base64 (without endless loop)


Is there a simple way to serialize an object using Jackson to base64 encoded JSON? (object -> JSON -> base64)

I tried using a custom StdSerializer, but this (of course) results in a endless loop:

class MySerializer extends StdSerializer<Foo> {
  public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) {
    StringWriter stringWriter = new StringWriter();
    JsonGenerator newGen = gen.getCodec().getFactory().createGenerator(stringWriter);
    gen.getCodec().getFactory().getCodec().writeValue(newGen, value);
    String json = stringWriter.toString();
    String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
    gen.writeString(base64);
  }
}

A workaround is to copy all fields to another class and use that class for the intermediate representation:

class TmpFoo {
  public String field1;
  public int field2;
  // ...
}

class MySerializer extends StdSerializer<Foo> {
  public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) {
    TmpFoo tmp = new TmpFoo();
    tmp.field1 = value.field1;
    tmp.field2 = value.field2;
    // etc.

    StringWriter stringWriter = new StringWriter();
    JsonGenerator newGen = gen.getCodec().getFactory().createGenerator(stringWriter);
    gen.getCodec().getFactory().getCodec().writeValue(newGen, tmp); // here "tmp" instead of "value"
    String json = stringWriter.toString();
    String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
    gen.writeString(base64);
  }
}

Creating a new ObjectMapper is not desired, because I need all registered modules and serializers of the default ObjectMapper.

I was hoping for some easier way of achieving this.


EDIT: Example

Step 1: Java Object

class Foo {
  String field1 = "foo";
  int field2 = 42;
}

Step 2: JSON

{"field1":"foo","field2":42}

Step 3: Base64

eyJmaWVsZDEiOiJmb28iLCJmaWVsZDIiOjQyfQ==

Solution

  • According to this site, there is a workaround to avoid this recursion problem:

    When we define a custom serializer, Jackson internally overrides the original BeanSerializer instance [...] our SerializerProvider finds the customized serializer every time, instead of the default one, and this causes an infinite loop.

    A possible workaround is using BeanSerializerModifier to store the default serializer for the type Folder before Jackson internally overrides it.

    If I understood the workaround correctly, your Serializer should look like this:

    class FooSerializer extends StdSerializer<Foo> {
    
        private final JsonSerializer<Object> defaultSerializer;
    
        public FooSerializer(JsonSerializer<Object> defaultSerializer) {
            super(Foo.class);
            this.defaultSerializer = defaultSerializer;
        }
    
        @Override
        public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            StringWriter stringWriter = new StringWriter();
            JsonGenerator tempGen = provider.getGenerator().getCodec().getFactory().createGenerator(stringWriter);
            defaultSerializer.serialize(value, tempGen, provider);
    
            tempGen.flush();
    
            String json = stringWriter.toString();
            String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
            gen.writeString(base64);
        }
    }
    

    In addition to the serializer, a modifier is needed:

    public class FooBeanSerializerModifier extends BeanSerializerModifier {
    
        @Override
        public JsonSerializer<?> modifySerializer(
          SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
    
            if (beanDesc.getBeanClass().equals(Foo.class)) {
                return new FooSerializer((JsonSerializer<Object>) serializer);
            }
            return serializer;
        }
    }
    

    Example module:

    ObjectMapper mapper = new ObjectMapper();
    
    SimpleModule module = new SimpleModule();
    module.setSerializerModifier(new FooBeanSerializerModifier());
    
    mapper.registerModule(module);
    

    EDIT:

    I've added flush() to flush the JsonGenerator tempGen. Also, I've created a minimal test enviroment with JUnit, which verifies your Example with Foo: The github repo can be found here.


    EDIT: Alternative 2

    Another (simple) option is using a wrapper class with generics:

    public class Base64Wrapper<T> {
    
        private final T wrapped;
    
        private Base64Wrapper(T wrapped) {
            this.wrapped = wrapped;
        }
    
        public T getWrapped() {
            return this.wrapped;
        }
    
        public static <T> Base64Wrapper<T> of(T wrapped) {
            return new Base64Wrapper<>(wrapped);
        }
    }
    
    public class Base64WrapperSerializer extends StdSerializer<Base64Wrapper> {
    
    
        public Base64WrapperSerializer() {
            super(Base64Wrapper.class);
        }
    
        @Override
        public void serialize(Base64Wrapper value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            StringWriter stringWriter = new StringWriter();
            JsonGenerator tempGen = provider.getGenerator().getCodec().getFactory().createGenerator(stringWriter);
            provider.defaultSerializeValue(value.getWrapped(), tempGen);
            tempGen.flush();
    
            String json = stringWriter.toString();
            String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
            gen.writeString(base64);
        }
    }
    

    An example usecase would be:

    final ObjectMapper mapper = new ObjectMapper();
    SimpleModule module = new SimpleModule();
    module.addSerializer(new Base64WrapperSerializer());
    mapper.registerModule(module);
    
    final Foo foo = new Foo();
    final Base64Wrapper<Foo> base64Wrapper = Base64Wrapper.of(foo);
    final String base64Json = mapper.writeValueAsString(base64Wrapper);
    

    This example can be found in this GitHub (branch: wrapper) repo, verifing you BASE64 String from your foo example with JUnit testing.