6

I have two classes like this:

public class A {
    String aProp = "aProp";

    public String getAProp() {
        return aProp;
    }
}

public class B {
    String bProp = "bProp";
    A a = new A();

    @JsonProperty("bProp")
    public String getBProp() {
        return bProp;
    }

    @JsonSerialize(using = CustomSerializer.class)
    public A getA() {
        return a;
    }     
}

I'm expecting to get JSON like this:

{
    "bProp": "bProp",         // just serizlised bProp
    "sProp1": "sProp1_aProp", // computed using aProp
    "sProp2": "sProp2_aProp"  // computed another way
}

So I wrote custom JsonSerializer like this:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class CustomSerializer extends JsonSerializer<A> {
    @Override
    public void serialize(A a, JsonGenerator json, SerializerProvider provider) throws IOException {
        json.writeStringField("sProp1", "sProp1_" + a.getAProp());
        json.writeStringField("sProp2", "sProp2_" + a.getAProp());
    }
}

But I keep getting an error:

com.fasterxml.jackson.core.JsonGenerationException: Can not write a field name, expecting a value

Unless I put json.writeStartObject(); and json.writeEndObject(); in serialize method (so it produces wrong JSON).

So I'm looking for a solution like @JsonUnwrapped to use with custom JsonSerializer.

panfil
  • 1,489
  • 3
  • 13
  • 19

2 Answers2

10

I understand your problem and the thing that you need is UnwrappingBeanSerializer. You can see another related SO post: Different JSON output when using custom json serializer in Spring Data Rest

The problem is that you cannot have both annotations @JacksonUnwrapped and @JsonSerialize in one field because when you have @JsonSerializer Jackson will always write field name.

Here is the complete solution:

public class CustomSerializer  extends UnwrappingBeanSerializer {
    public CustomSerializer(BeanSerializerBase src, NameTransformer transformer) {
        super(src, transformer);
    }

    @Override
    public JsonSerializer<Object> unwrappingSerializer(NameTransformer transformer) {
        return new CustomSerializer(this, transformer);
    }

    @Override
    protected void serializeFields(Object bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        A a = (A) bean;
        jgen.writeStringField("custom", a.getAProp());
        jgen.writeStringField("custom3", a.getAProp());
    }

    @Override
    public boolean isUnwrappingSerializer() {
        return true;
    }

}

Test case, you should redefine your object mapper with custom configuration or research for other method .

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringApplicationConfiguration(classes = Application.class)
public class ColorsTest {

    ObjectMapper mapper = new ObjectMapper();

    @Before
    public void setUp(){
        mapper.registerModule(new Module() {
            @Override
            public String getModuleName() {
                return "my.module";
            }

            @Override
            public Version version() {
                return Version.unknownVersion();
            }

            @Override
            public void setupModule(SetupContext context) {

                context.addBeanSerializerModifier(new BeanSerializerModifier() {
                    @Override
                    public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
                        if(beanDesc.getBeanClass().equals(A.class)) {
                            return new CustomSerializer((BeanSerializerBase) serializer, NameTransformer.NOP);
                        }
                        return serializer;
                    }
                });

            }
        });
    }
    @Test
    public void testSerializer() throws JsonProcessingException {
        System.out.println(mapper.writeValueAsString(new B()));
    }
}

Class B:

public class B {

        @JsonProperty("bProp")
        public String getBProp() {
            return "bProp";
        }


    @JsonUnwrapped
        public A getA() {
            return new A();
        }
}
mkobit
  • 43,979
  • 12
  • 156
  • 150
Nikolay Rusev
  • 4,060
  • 3
  • 19
  • 29
  • Thanks, Nikolay. One more thing: how to make Jackson module agnostic of the concrete class A? So it would check not that `beanDesc.getBeanClass().equals(A.class)` but presense of some custom annotation on the property? So it would use CustomSerializer when serializing `a` prop in `B` but don't if it is another class or property. – panfil Jun 16 '15 at 09:56
  • http://fasterxml.github.io/jackson-databind/javadoc/2.2.0/com/fasterxml/jackson/databind/BeanDescription.html here is documentation BeanDescription has method getClassAnnotations() and you will get it ;) – Nikolay Rusev Jun 16 '15 at 10:04
  • I can't use `getClassAnnotations()`, I need annotations of the property method `getA()` in `B` class and not annotations of the `A` class itself. Reflection has no use for that. – panfil Jun 16 '15 at 10:22
  • To get access to annotations on property, or enclosing class, implement `ContextualSerializer`; `createContextual()` gets passed property definition if one exists (does not when serializing root values), through with annotations can be accessed. This also include mix-ins, hierarchic overrides, so ideally you never need to use reflection directly via `Method` or `Field` – StaxMan Jun 16 '15 at 18:29
5

I like to add this post and solution to the question asked here: Using custom Serializers with JsonUnwrapperd as the original poster is using JsonSerializer as I am. The suggest approach with the UnwrappingBeanSerializer won't work in this case. My post has a slightly different goal, but the idea from the post should be applicable to your use case easily, as it is just overwriting one more method and not having to add bunch of stuff apart from JsonUnwrapped on the property.

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.io.IOException;

public class Test {

    static class A {
        String aProp = "aProp";

        public String getAProp() {
            return aProp;
        }
    }

    static class B {
        String bProp = "bProp";
        A a = new A();

        @JsonProperty("bProp")
        public String getBProp() {
            return bProp;
        }

        @JsonSerialize(using = CustomSerializer.class)
        @JsonUnwrapped
        public A getA() {
            return a;
        }
    }


    static class CustomSerializer extends JsonSerializer<A> {
        @Override
        public boolean isUnwrappingSerializer() {
            return true;
        }

        @Override
        public void serialize(A a, JsonGenerator json, SerializerProvider provider) throws IOException {
            json.writeStringField("sProp1", "sProp1_" + a.getAProp());
            json.writeStringField("sProp2", "sProp2_" + a.getAProp());
        }
    }

    public static void main(String... a) throws Exception {
        final ObjectMapper o = new ObjectMapper();
        o.enable(SerializationFeature.INDENT_OUTPUT);
        System.out.println(o.writeValueAsString(new B()));
    }   
}
Michael Simons
  • 4,640
  • 1
  • 27
  • 38