Spring MVC & JSON Serialization

Spring uses Jackson internally for serializing and deserializing data in json format. It just works in most cases. Still, some corner cases can turn up where Spring cannot apply Jackson’s default operations. That is when you must write your own functions.

In this posting I will show you how to customize json serialization in the context of the Spring MVC. The code is available on Github.

The Use Case

I will demonstrate a typical shop example where a customer adds a product to the basket. Below you find the BasketItem class and the Controller. BasketItem is used in request and response. I want to point out the importance of the annotation @RequestBody. If you do not set that, then Spring does not invoke Jackson at all.

public class BasketItem {
  private String product;
  private String code;
  private int amount;

  public BasketItem() { }

  public String getProduct() { return product; }
  public void setProduct(String product) { this.product = product; }

  public String getCode() { return code; }
  public void setCode(String code) { this.code = code; }

  public int getAmount() { return amount; }
  public void setAmount(int amount) { this.amount = amount; }
}

@RestController
public class BasketController {
  @RequestMapping(path = "/addToBasket")
  public List addToBasket(@RequestBody BasketItem basketItem) {
    return Arrays.asList(basketItem);
  }
}

The client sends and expects json that is a little bit different from the BasketItem so we need both custom serialization and deserialization:

{
  "detail": {
    "product": "car",
    "code": "car-01"
  },
  "amount": 1
}

Jackson relies on the so-called ObjectMapper. If you want customized behavior for certain classes you have to create a Jackson module that contains the relevant logic and register it to the ObjectMapper.

In Spring things are a little bit different. You do not have direct access to the ObjectMapper. You cannot inject it as a bean either. The only solution is to create a Jackson module and mark it as a bean. Spring automatically adds that to the internal ObjectMapper.

Jackson modules are quite powerful in their possibilities. However, if it comes down to just registering a serializer, then the easiest way is to extend from SimpleModule. It includes all the code required to be an actual Jackson module.

Implementing the Serialization Logic

Next we tackle the implementation. As we follow TDD practices, we begin with the unit tests. In the tests we specify that there exists a Jackson module with the name BasketJsonModule which is registered to the ObjectMapper. Afterwards serialization should work as expected:

public class BasketItemTest {
  @Test
  public void deserialize() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new BasketJsonModule());

    BasketItem basketItem = objectMapper.readValue(
      "{\"detail\": {\"product\": \"car\", \"code\": \"car-01\"}, \"amount\": 1}",
      BasketItem.class);

    assertEquals("car" , basketItem.getProduct());
    assertEquals("car-01", basketItem.getCode());
    assertEquals(1, basketItem.getAmount());
  }

  @Test
  public void serialize() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new BasketJsonModule());

    BasketItem basketItem = new BasketItem();
    basketItem.setAmount(5);
    basketItem.setProduct("car");
    basketItem.setCode("car-02");

    String json = objectMapper.writeValueAsString(basketItem);
    assertEquals(
      "{\"detail\":{\"product\":\"car\",\"code\":\"car-02\"},\"amount\":5}",
      json);
  }
}

So far so good. Now we implement the classes according to the unit tests and make sure the tests turn green:

public class BasketItemDeserializer extends JsonDeserializer {
  @Override
  public BasketItem deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
    throws IOException, JsonProcessingException {
    ObjectCodec objectCodec = jsonParser.getCodec();
    JsonNode jsonNode = objectCodec.readTree(jsonParser);

    BasketItem basketItem = new BasketItem();
    basketItem.setProduct(jsonNode.get("detail").get("product").asText());
    basketItem.setCode(jsonNode.get("detail").get("code").asText());
    basketItem.setAmount(jsonNode.get("amount").asInt());

    return basketItem;
  }
}

public class BasketItemSerializer extends JsonSerializer {
  @Override
  public void serialize(
    BasketItem basketItem, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
      throws IOException, JsonProcessingException {
    jsonGenerator.writeStartObject();
    jsonGenerator.writeObjectFieldStart("detail");
    jsonGenerator.writeStringField("product", basketItem.getProduct());
    jsonGenerator.writeStringField("code", basketItem.getCode());
    jsonGenerator.writeEndObject();
    jsonGenerator.writeNumberField("amount", basketItem.getAmount());
    jsonGenerator.writeEndObject();
  }
}

@Service
public class BasketItemJsonModule extends SimpleModule {
  public BasketItemJsonModule() {
    this.addDeserializer(BasketItem.class, new BasketItemDeserializer());
    this.addSerializer(BasketItem.class, new BasketItemSerializer());
  }
}

Running the Application

Since unit tests are running fine, our last step will be to test the application as a whole. Please note that the BasketItemJsonModule is already annotated with @Service. Spring automatically registers it to its internal ObjectMapper instance.

From the terminal we run:

./mvnw spring-boot:run

After Spring is running we open a second terminal and execute the following curl command:

curl -X POST \
  http://localhost:8080/addToBasket \
  -H 'content-type: application/json' \
  -d '{
    "detail": {
      "product": "car",
      "code": "car-02"
    },
    "amount": 1
   }'

If the json structure with the right values is returned, you have succeeded.

Alternative Approach: Using @JsonSerialize and @JsonDeserialize

A short cut sets the serializer and deserializer by adding the following annotations to the BasketItem class:

@JsonDeserialize(using = BasketItemDeserializer.class)
@JsonSerialize(using = BasketItemSerializer.class)
public class BasketItem {
...

This approach is shorter and very straightforward since you do not require a Jackson module. In my opinion, however, it should only be used for simple applications. In systems composed of multiple modules, you do not want to spread dependencies to Jackson. It is also quite possible that you will not have access to these classes in order to set the annotations.

Why ObjectMapper cannot be accessed directly

When first tackling this problem my strategy was to find ways to access the ObjectMapper directly. After all, Spring lets you inject virtually everything. That seemed the natural way to me.

The problem is, Spring itself creates a new instance of the ObjectMapper in each request. Therefore you can only provide a Factory class as a bean for the ObjectMapper. I do not recommend doing that. The Spring team knows the best configuration that integrates and works well with the rest of the Spring ecosystem.

If you are still curious you can check out the AbstractJackson2HttpConverter and classes it uses. The documentation is also worth a check.

I will repeat this as my closing advice: Don’t miss the @RequestBody in the Controller.

Leave a Reply