Different JSON output when using custom json serializer in Spring Data Rest
Asked Answered
O

4

13

After adding a custom Jackson serializer based on the official documenation I've observed a slightly different json output format.

This example is based on spring-restbucks.

Extend org.springsource.restbucks.WebConfiguration from RepositoryRestMvcConfiguration and override configureJacksonObjectMapper:

@Override
protected void configureJacksonObjectMapper(ObjectMapper objectMapper) {
    final SimpleSerializers serializers = new SimpleSerializers();
    serializers.addSerializer(Order.class, new OrderSerializer());
    objectMapper.registerModule(new SimpleModule("CustomSerializerModule"){
        @Override public void setupModule(SetupContext context) {
            context.addSerializers(serializers);
        }
    });
}

Create class org.springsource.restbucks.order.OrderSerializer. For the sake of brevity just write attribute paid as JSON.

public class OrderSerializer extends JsonSerializer<Order> {
    @Override
    public void serialize(Order value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
        jgen.writeStartObject();
        jgen.writeBooleanField("paid", value.isPaid());
        jgen.writeEndObject();
    }
}

Before adding OrderSerializer json response for http://localhost:8080/orders/1 looks like:

{
  "location": "TAKE_AWAY",
  "status": "PAYMENT_EXPECTED",
  "orderedDate": "2014-03-24T15:05:09.988+01:00",
  "items": [
    {
      "name": "Java Chip",
      "quantity": 1,
      "milk": "SEMI",
      "size": "LARGE",
      "price": {
        "currency": "EUR",
        "value": 4.2
      }
    }
  ],
  "_links": {
    ...
  }
}

After adding OrderSerializer json response for http://localhost:8080/orders/1 looks like

{
  "content": {
    "paid": false
  },
  "_links": {
    ...
  }
}

The main pinpoint is that attribute paid is wrapped into another object content which is an attribute of org.springframework.hateoas.Resource. I've expected a response without this attribute:

{
  "paid": false,  
  "_links": {
    ...
  }
}

I've looked into Jackson code and found that UnwrappingBeanSerializer might be the solution I'm looking for. After looking at how to initialize UnwrappingBeanSerializer I think that this serializer is not meant to be subclassed for custom use.

I would like to know whether this deviating json format when using a custom serializer is a normal behaviour or a bug in Spring Data Rest. Any kind of help is appreciated.

Oberg answered 24/3, 2014 at 15:5 Comment(0)
D
10

This is not a bug of Spring Data Rest it is actually the normal behaviour of the Jackson Serializer. Whenever you use the @JsonUnwrapped Annotation (as the Resource content field does) together with a custom Serializer the Jackson Serializer will explicitly write the field name (in this case content). Have a look at the UnwrappingBeanPropertyWriter for more details. Anyhow you have been on the right track using the UnwrappingBeanSerializer but the setup is slightly different then the usual Serializer registration. The following example should fix your problem:

@Override
protected void configureJacksonObjectMapper(ObjectMapper objectMapper) {
    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(Order.class)) {
                        return new UnwrappingOrderSerializer((BeanSerializerBase) serializer, NameTransformer.NOP);
                    }
                    return serializer;
                }
            });
        }
    });
}

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

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

    @Override
    protected void serializeFields(Object bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        Order order = (Order) bean;
        jgen.writeStringField("paid", order.isPaid();
    }

    @Override
    public boolean isUnwrappingSerializer() {
        return true;
    }
}
Devol answered 14/3, 2015 at 10:40 Comment(2)
Thanks for your answer. It works as expected. There's a compile error in your example at jgen.writeStringField("paid", order.isPaid();. You missed the closing parenthesis.Oberg
I'm getting an error: ``` Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.in.e.i18n.TranslationSerializer': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {} ``` Any ideas?Adlare
C
3

Projection is one solution and overriding one method of JsonSerializer is another:

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

Then you should be able to omit the start and end of an object.

Find my blog post here.

Confraternity answered 22/12, 2017 at 12:41 Comment(0)
H
2

Beyond andreast00's solution above - make sure to also override the other constructors and with... methods as well or it may create a default UnwrappingBeanSerializer in the background and ignore your custom serialization code:

public UnwrappingOrderSerializer(UnwrappingBeanSerializer src, ObjectIdWriter objectIdWriter) {
    super(src, objectIdWriter);
}

public UnwrappingOrderSerializer(UnwrappingBeanSerializer src, ObjectIdWriter objectIdWriter, Object filterId) {
    super(src, objectIdWriter, filterId);
}

public UnwrappingOrderSerializer(UnwrappingBeanSerializer src, Set<String> toIgnore) {
    super(src, toIgnore);
}



@Override
public BeanSerializerBase withObjectIdWriter(ObjectIdWriter objectIdWriter) {
    return new UnwrappingOrderSerializer(this, objectIdWriter);
}

@Override
public BeanSerializerBase withFilterId(Object filterId) {
    return new UnwrappingOrderSerializer(this, this._objectIdWriter, filterId);
}

@Override
protected BeanSerializerBase withIgnorals(Set<String> toIgnore) {
    return new UnwrappingOrderSerializer(this, toIgnore);
}

Also, depending on whether the serialisation is occurring from a @JsonUnwrapped object, as an object in an array or as a single object the jsonGenerater.writeStartObject may need to be calleddepends on weather the object has been started or not. I used:

boolean writeStartEnd = !jsonGenerator.getOutputContext().inObject() 
    || jsonGenerator.getOutputContext().getCurrentName() != null;

if (writeStartEnd) jsonGenerator.writeStartObject(entity);

...serialisation code...

if (writeStartEnd) {    
    jsonGenerator.writeEndObject();
}
Hybridism answered 12/10, 2017 at 6:48 Comment(1)
You have a bit of content there. Simply change wording, like: "beyond the other answer by" ... for example. Otherwise people will soon flag this is as "not an answer" and probably downvote it. Use answers for answers!Turning
I
0

I had the same problem. Instead of using Jackson Serializer , I Have used @Projecton and customized my output. You can find a reference here

Igniter answered 3/1, 2015 at 9:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.