RESTEasy BeanValidationエラーメッセージのカスタマイズ
JAX-RS Specでは、BeanValidationのバリデーションエラー時には、エラー原因を返すことのみが規定されており、メッセージのフォーマット自体は実装依存です。
7.6 Validation and Error Reporting
In all cases, JAX-RS implementations SHOULD include a response entity describing the source of the error; however, the exact content and format of this entity is beyond the scope of this specification.
この記事ではRESTEasyの場合にどのようにメッセージフォーマットをカスタマイズするか言及します。
RESTEasyデフォルトの振る舞い
例として以下のバリデーションを実行を考えます。
public class Customer { @NotBlank(message = "Customer id is required.") private String id; @NotBlank(message = "Customer name is required.") private String name; @NotBlank(message = "Customer location is required.") private String location; ... } @Path("/customer") public class CustomerResource { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Customer post(@Valid Customer cu) { ... } }
WildFlyやQuarkusが内蔵するJAX-RS実装のRESTEasyの場合、バリデーションエラー時にはHTTPレスポンスヘッダにvalidation-exception:
を含むと共に、以下のようなフォーマットのメッセージを返します。各プロパティはバリデーションの種類を示しています。例の場合はメソッドパラメータでバリデーションしているため、
trueparameterViolations
にメッセージが含まれています。
// 見やすいように改行を含めていますが、実際には改行は含まれません { "exception":null, "fieldViolations":[], "propertyViolations":[], "classViolations":[], "parameterViolations":[ { "constraintType":"PARAMETER", "path":"post.arg0.name", "message":"Customer name is required.", "value":"" }, { "constraintType":"PARAMETER", "path":"post.arg0.id", "message":"Customer id is required.", "value":"" } ], "returnValueViolations":[] }
エラーメッセージのカスタマイズ
もっとシンプルに以下のようなエラーメッセージのリストを返すことを考えます。
{"errors":["Customer id is required.","Customer name is required."]}
org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapperクラスをオーバーライドするとメッセージフォーマットのカスタマイズができます。
コード例は以下の通りです。ResteasyViolationExceptionMapperクラスでは後で拡張できるように、いくつかのメソッドのスコープがprotectedに設定されています。WildFly17で動作します。Quarkus0.16.1では原因は詳しくデバッグしていませんが動作しません。
package sample; import org.jboss.resteasy.api.validation.ResteasyViolationException; import org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapper; import org.jboss.resteasy.api.validation.Validation; import javax.json.Json; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.ext.Provider; import java.util.List; import java.util.stream.Collectors; @Provider public class ViolationExceptionMapper extends ResteasyViolationExceptionMapper { @Override protected Response buildViolationReportResponse(ResteasyViolationException exception, Response.Status status) { ResponseBuilder builder = Response.status(status); builder.header(Validation.VALIDATION_HEADER, "true"); if (isAcceptJson(exception.getAccept())) { builder.type(MediaType.APPLICATION_JSON); List<String> errors = exception.getViolations() .stream() .map(violation -> violation.getMessage()) .collect(Collectors.toList()); builder.entity(Json.createObjectBuilder() .add("errors", Json.createArrayBuilder(errors).build()) .build()); return builder.build(); } // Default media type. builder.type(MediaType.TEXT_PLAIN); builder.entity(exception.toString()); return builder.build(); } private boolean isAcceptJson(List<MediaType> accepts) { return accepts.stream() .anyMatch(mt -> MediaType.APPLICATION_JSON_TYPE.getType().equals(mt.getType()) && MediaType.APPLICATION_JSON_TYPE.getSubtype().equals(mt.getSubtype())); } }
org.jboss.resteasy.api.validation
パッケージにクラスパスが通るように、pom.xmlのdependencyにresteasy-jaxrs
を追加します。
<dependencies> <dependency> <groupId>org.jboss.spec</groupId> <artifactId>jboss-javaee-web-8.0</artifactId> <version>1.0.3.Final</version> <type>pom</type> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jaxrs</artifactId> <version>3.7.0.Final</version> <scope>provided</scope> </dependency> </dependencies>
まとめ
- JAX-RS仕様ではBeanValidationエラー時のメッセージフォーマットは規定されておらず実装依存
- RESTEasyの場合、エラー時にはHTTPレスポンスに
validation-exception : true
を含み、独自フォーマットのメッセージを返す - メッセージフォーマットのカスタマイズには
org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapper
の継承クラスを作る - 参考 RESTEasyドキュメント Chapter 55. Validation