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を受け取る例。
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用の設定をプロパティーファイルで記述することが出来る。(Spring Boot 1.5.6)
参考: Spring MVCドキュメントのCustomize the Jackson ObjectMapper
# 値が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の設定は無視されるようだ。
オブジェクトとJSONとの変換はObjectMapperクラスで行われる。(Spring Boot 1.5.6)
ObjectMapperを生成するJackson2ObjectMapperBuilderに対して、Jackson用の設定を行うことが出来る。
参考: stackoverflowのHow to customise the Jackson JSON mapper implicitly used by Spring Boot?
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の設定は無視されるようだ。
JavaBean(Entityクラス)にJacksonの設定を記述することが出来る。(Spring Boot 1.5.6)
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のバージョンによって異なることもあるので注意)
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)
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を作成する。
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というのも、元データにそんなに桁数無いし…。
クライアントに詳細なメッセージを返すのには向いてなさそう。
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", | 
		string_value = "a" | 
		正常 | 
{ "string_value" : 9, | 
		string_value = "9" | 
		JSONは数値→EntityはString | 
{ "string_value" : "a", | 
		string_value = "a" | 
		JSONは文字列→Entityはint | 
{ "string_value" : [], | 
		エラー | JSONは空配列→EntityはString | 
string_value = null | 
		
		DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT=true | 
	|
{ "string_value" : ["a"], | 
		エラー | JSONは要素1の配列→EntityはString | 
string_value = "a" | 
		
		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へ代入できないのでエラーになる(爆)