S-JIS[2017-09-02] 変更履歴

Spring Boot Jackson

Spring BootでのJacksonの設定方法のメモ。


概要

Spring Boot(1.5.6)では、JSONはJacksonで操作している。

Rest APIのリクエストでJSONを受け取る場合、RestControllerクラスのメソッドの引数にはJavaBeans(Entityクラス)やMapを指定できる。
また、レスポンスでJSONを返す際に、RestControllerクラスのメソッドの戻り値にJavaBeans(Entityクラス)やMapを使用できる。
この、JavaBeans(Entityクラス)やMapとJSONとの変換にJacksonが使われる。


RestControllerでJSONを受け取る例。

src/main/java/com/example/demo/rest/ApiController.java:

package com.example.demo.rest;

import java.util.Map;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.JsonNode;
@RestController
public class ApiController {
	@RequestMapping(path = "/api/example1")
	public String example1(@RequestBody ExampleEntity arg) {
		return "ok from example1";
	}
	@RequestMapping(path = "/api/example2")
	public String example2(@RequestBody Map<String, Object> arg) {
		return "ok from example2";
	}
	@RequestMapping(path = "/api/example3")
	public String example3(@RequestBody JsonNode arg) {
		return "ok from example3";
	}
}

JavaBeans(Entityクラス)やMapとJSONとの変換を自前で行いたい場合は、ObjectMapperを利用する。

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
	@Autowired
	private ObjectMapper objectMapper;
	@RequestMapping(path = "/api/example1")
	public String example1(@RequestBody ExampleEntity arg) {

		@SuppressWarnings("unchecked")
		Map<String, Object> map1 = objectMapper.convertValue(arg, Map.class);

		Map<String, Object> map2 = objectMapper.convertValue(arg, new TypeReference<Map<String, Object>>() {
		});

		return "ok from example1";
	}
	@RequestMapping(path = "/api/example2")
	public String example2(@RequestBody Map<String, Object> arg) {

		ExampleEntity entity = objectMapper.convertValue(arg, ExampleEntity.class);

		return "ok from example2";
	}
	@RequestMapping(path = "/api/example3")
	public String example3(@RequestBody JsonNode arg) {

		ExampleEntity entity1 = objectMapper.convertValue(arg, ExampleEntity.class);

		ExampleEntity entity2;
		try {
			entity2 = objectMapper.readValue(arg.traverse(), ExampleEntity.class);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}

		return "ok from example3";
	}

プロパティーファイルでのJacksonの設定

Jackson用の設定をプロパティーファイルで記述することが出来る。(Spring Boot 1.5.6)

参考: Spring MVCドキュメントのCustomize the Jackson ObjectMapper

src/main/resources/application.properties:

# 値がnullのプロパティーを出力しない
spring.jackson.default-property-inclusion=NON_NULL

# JSON出力時に改行・インデントを入れる
spring.jackson.serialization.INDENT_OUTPUT=true

spring.jackson.default-property-inclusionはInclude列挙型の値。
NON_NULLにすると、値がnullのプロパティーはプロパティー自体が出力されなくなる。
設定値は小文字でも動作するようだが、STSの補完機能では大文字でないとエラーになる。

spring.jackson.serializationにはSerializationFeature列挙型に対してtrue/falseを設定する。
INDENT_OUTPUTをtrueにすると、レスポンスボディーに出力したJSONには改行・インデントが入るようになる。(いわゆるPretty Printing)
INDENT_OUTPUTの部分は小文字でも動作するようだが、STSの補完機能では大文字でないとエラーになる。もしくは「indent-output」でも可能なようだ。

同様に、spring.jackson.deserializationにはDeserializationFeature列挙型に対してtrue/falseを設定する。


なお、ObjectMasterを自分でインスタンス化している場合はapplication.propertiesの設定は無視されるようだ。


ObjectMapperでのJacksonの設定

オブジェクトとJSONとの変換はObjectMapperクラスで行われる。(Spring Boot 1.5.6)
ObjectMapperを生成するJackson2ObjectMapperBuilderに対して、Jackson用の設定を行うことが出来る。

参考: stackoverflowのHow to customise the Jackson JSON mapper implicitly used by Spring Boot?

src/main/java/com/example/demo/config/JacksonConfig.java:

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.SerializationFeature;
@Configuration
public class JacksonConfig {

	@Bean
	public Jackson2ObjectMapperBuilder objectMapperBuilder() {
		Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();

		// 値がnullのプロパティーを出力しない
		builder.serializationInclusion(JsonInclude.Include.NON_NULL);

		// JSON出力時に改行・インデントを入れる
		builder.indentOutput(true);
		// builder.featuresToEnable(SerializationFeature.INDENT_OUTPUT);
		// builder.featuresToDisable(SerializationFeature.INDENT_OUTPUT); // 改行・インデントを入れない

		return builder;
	}
}

Jacksonの設定を行うクラスを自作し、Jackson2ObjectMapperBuilderを返すメソッドを作る。

Jackson2ObjectMapperBuilderに対してJacksonの各種設定が行えるようになっている。
(Jackson2ObjectMapperBuilderの内部でObjectMapperに設定される)


Jackson2ObjectMapperBuilderに自分が設定したい内容用のメソッドが無い場合、ObjectMapperに直接設定するには以下の様にすれば良さそう。

import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
public class JacksonConfig {

	@Bean
	public Jackson2ObjectMapperBuilder objectMapperBuilder() {
		Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() {

			@Override
			public void configure(ObjectMapper objectMapper) {
				super.configure(objectMapper);

				// ここでObjectMapperに対する設定が書ける
			}
		};

		return builder;
	}
}

なお、ObjectMasterを自分でインスタンス化している場合はapplication.propertiesの設定は無視されるようだ。


個別のJacksonの設定

JavaBean(Entityクラス)にJacksonの設定を記述することが出来る。(Spring Boot 1.5.6)

src/main/java/com/example/demo/entity/ExampleEntity.java:

package com.example.demo.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonInclude(JsonInclude.Include.NON_NULL)	// 値がnullのプロパティーを出力しない
@JsonPropertyOrder({ "value1", "v2", "null1", "null2" })	// プロパティーの出力順
public class ExampleEntity {

	public String value1;

	@JsonProperty("v2")	// プロパティー名を指定
	public String value2;

	@JsonIgnore	// このプロパティーは出力しない 
	public String value3;

	public String null1;

	@JsonInclude(JsonInclude.Include.ALWAYS)	// 常に出力する
	public String null2;
}

@JsonIncludeアノテーションはクラスにもフィールド個別にも指定できる。

@JsonPropertyOrderアノテーションを指定しない場合、JSONとして出力されるときのプロパティーの並び順は不定。
(並び順はリフレクションに依存する。Javaのバージョンによって異なることもあるので注意)

src/main/java/com/example/demo/rest/ApiController.java:

package com.example.demo.rest;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.entity.ExampleEntity;
@RestController
public class ApiController {

	@RequestMapping(path = "/api/example4")
	public ExampleEntity example4() {
		ExampleEntity entity = new ExampleEntity();
		entity.value1 = "foo";
		entity.value2 = "bar";
		entity.value3 = "zzz";
		entity.null1 = null;
		entity.null2 = null;

		return entity;
	}
}

↓実行結果

$ curl http://127.0.0.1:8080/api/example4
{
  "value1" : "foo",
  "v2" : "bar",
  "null2" : null
}

列挙型の変換

Jacksonでは列挙型(enum)も扱う事が出来る。(Spring Boot 1.5.6)

src/main/java/com/example/demo/rest/ApiController.java:

package com.example.demo.rest;

import java.util.Collections;
import java.util.Map;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ApiController {
	private enum ExampleEnum {
		STATUS1, STATUS2, STATUS3
	}
	@RequestMapping(path = "/api/enum")
	public Map<String, ExampleEnum> enumExample() {
		return Collections.singletonMap("status", ExampleEnum.STATUS1);
	}
}

↓実行例

$ curl http://127.0.0.1:8080/api/enum
{
  "status" : "STATUS1"
}

列挙型の値に対し、JSON上で使用する値を指定することが出来る。

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
	private enum ExampleEnum {
		STATUS1, STATUS2, STATUS3;
		@JsonValue
		public String value() {
			return name().toLowerCase(); // JSONには小文字で出力する
		}
		@JsonCreator
		public static ExampleEnum fromValue(String value) {
			return valueOf(value.toUpperCase());
		}
	}

↓実行例

$ curl http://127.0.0.1:8080/api/enum
{
  "status" : "status1"
}

列挙型の定義の中に@JsonValueアノテーションを付けたメソッドを用意し、JSONで出力する値を返すようにする。
逆に、@JsonCreatorアノテーションを付けたメソッドで、JSONの値を列挙型に変換する。

参考: ApplePedlarさんのJacksonを使ってみた


例外処理

リクエストのJSONが不正な形式の場合(JSONとしてエラーになる場合)はJSONのパース時に例外が発生する。
デフォルトでは、エラー情報のJSONがクライアントに返る。(Spring Boot 1.5.6)

エラー情報のレスポンスの例:

$ curl http://127.0.0.1:8080/api/example1 -X POST -H 'Content-Type: application/json' -d '{ "value1": "zzz" '	←末尾の閉じ括弧が無い
{
  "timestamp" : 1504308267579,
  "status" : 400,
  "error" : "Bad Request",
  "exception" : "org.springframework.http.converter.HttpMessageNotReadableException",
  "message" : "JSON parse error: Unexpected end-of-input: expected close marker for Object (start marker at [Source: java.io.PushbackInputStream@77fb63bd; line: 1, column: 1]); nested exception is com.fasterxml.jackson.core.io.JsonEOFException: Unexpected end-of-input: expected close marker for Object (start marker at [Source: java.io.PushbackInputStream@77fb63bd; line: 1, column: 1])\n at [Source: java.io.PushbackInputStream@77fb63bd; line: 1, column: 37]",
  "path" : "/api/example1"
}

エラーとなった箇所の情報はまだしも、例外のクラス名等(HttpMessageNotReadableExceptionやJsonEOFException)がそのまま出てしまうのはちょっといただけない^^;


HttpMessageNotReadableExceptionを自前で処理する為にはExceptionHandlerを作成する。

src/main/java/com/example/demo/api/ApiExceptionHandler.java:

package com.example.demo.rest;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonMappingException.Reference;
@RestControllerAdvice
public class ApiExceptionHandler {

	@ExceptionHandler(HttpMessageNotReadableException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public Map<String, Object> handleException(HttpMessageNotReadableException exception) {
		Throwable cause = exception.getCause();
		if (!(cause instanceof JsonProcessingException)) {
			throw exception; // 自分で処理しないので再スロー
		}

		JsonProcessingException e = (JsonProcessingException) cause;
		JsonLocation location = e.getLocation();

		Map<String, Object> body = new LinkedHashMap<>();
		body.put("message", "invalid JSON");
		body.put("line", location.getLineNr());
		body.put("column", location.getColumnNr());

		if (e instanceof JsonMappingException) {
			JsonMappingException m = (JsonMappingException) e;
			List<Reference> path = m.getPath();
			if (!path.isEmpty()) {
				body.put("fieldName", path.get(path.size() - 1).getFieldName());
			}
		}

		body.put("detail", e.getOriginalMessage());

		return body;
	}
}

↓実行例

$ curl http://127.0.0.1:8080/api/example1 -X POST -H 'Content-Type: application/json' -d '{ "value1": "zzz" '	←末尾の閉じ括弧が無い
{
  "message" : "invalid JSON",
  "line" : 1,
  "column" : 39,
  "detail" : "Unexpected end-of-input: expected close marker for Object (start marker at [Source: java.io.PushbackInputStream@b1dc5c3; line: 1, column: 1])"
}

エラーメッセージの中にPushbackInputStreamとかのクラス名が出てくるのはどうしようもないな(苦笑)
column=39というのも、元データにそんなに桁数無いし…。
クライアントに詳細なメッセージを返すのには向いてなさそう。


DeserializationProblemHandler

JSONをJavaBeans(Entityクラス)に変換する際は、型がある程度違っても自動的に変換してくれる。

例えばJSON上の数値はStringに入れてくれるし、JSON上は文字列であっても数字であればintに変換してくれる。
しかしJSON上が配列だと、Stringやintには変換できずにエラーになる。

public class ExampleEntity5 {

	public String string_value;

	public int int_value;
}
JSON Entity 備考
{ "string_value" : "a",
  "int_value" : 1 }
string_value = "a"
int_value = 1
正常
{ "string_value" : 9,
  "int_value" : 1 }
string_value = "9"
int_value = 1
JSONは数値→EntityはString
{ "string_value" : "a",
  "int_value" : "1" }
string_value = "a"
int_value = 1
JSONは文字列→Entityはint
{ "string_value" : [],
  "int_value" : "1" }
エラー JSONは空配列→EntityはString
string_value = null
int_value = 1
DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT=true
{ "string_value" : ["a"],
  "int_value" : "1" }
エラー JSONは要素1の配列→EntityはString
string_value = "a"
int_value = 1
DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS=true

こういったエラーをハンドリングするのがDeserializationProblemHandler。

DeserializationProblemHandlerの各メソッドは、通常の変換が出来ないときに呼ばれる。
これをオーバーライドしておけばエラー時の処理が出来る。
これらのメソッドで変換後の値を返せば、その値が使われる。

@Configuration
public class JacksonConfig {

	@Bean
	public Jackson2ObjectMapperBuilder objectMapperBuilder() {
		Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() {

			@Override
			public void configure(ObjectMapper objectMapper) {
				super.configure(objectMapper);

				objectMapper.addHandler(new MyHandler());
			}
		};

		return builder;
	}
	private static class MyHandler extends DeserializationProblemHandler {

		// ARRAY to String, int
		@Override
		public Object handleUnexpectedToken(DeserializationContext ctxt, Class<?> targetType, JsonToken t, JsonParser p, String failureMsg) throws IOException {
			switch (t) {
			case START_ARRAY:
				skipToEnd(p, JsonToken.END_ARRAY);
				break;
			case START_OBJECT:
				skipToEnd(p, JsonToken.END_OBJECT);
				break;
			default:
				break;
			}
			return null;
		}

		private void skipToEnd(JsonParser p, JsonToken end) throws IOException {
			for (;;) {
				JsonToken t = p.nextToken();
				if (t == end) {
					return;
				}
				switch (t) {
				case START_ARRAY:
					skipToEnd(p, JsonToken.END_ARRAY);
					break;
				case START_OBJECT:
					skipToEnd(p, JsonToken.END_OBJECT);
					break;
				default:
					break;
				}
			}
		}

handleUnexpectedTokenメソッドは、例えば変換先(Entityのフィールド)がStringやintなのに配列の開始「[」やオブジェクトの開始「{」が来たときに呼ばれる。
ここでnullを返せば、Entityのフィールドにnullが代入される。
ただし、 handleUnexpectedTokenはトークン単位で処理するものなので、配列やオブジェクトの場合はトークンを読み飛ばしてやる必要がある。
面倒…というか、何か漏れがあるような気がして不安(爆)
 

		// String to int
		@Override
		public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType, String valueToConvert, String failureMsg) throws IOException {
			return null;
		}
	}
}

handleWeirdStringValueメソッドは、JSONがStringだが変換先が例えばintの場合に呼ばれる。

しかし、プリミティブ型(int等)の場合は上手く処理できない。
このメソッドを呼び出した側は、このメソッドの戻り値が要求された型なのかチェックするのだが、
intの場合は(メソッドの戻り値の型がObjectなのでボクシングにより)Integerで返ることになり、intではないのでエラーになる(爆)
また、nullを返すと、intへ代入できないのでエラーになる(爆)


Spring Bootへ戻る / Spring Frameworkへ戻る / 技術メモへ戻る
メールの送信先:ひしだま