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へ代入できないのでエラーになる(爆)