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:
true
を含むと共に、以下のようなフォーマットのメッセージを返します。各プロパティはバリデーションの種類を示しています。例の場合はメソッドパラメータでバリデーションしているため、parameterViolationsにメッセージが含まれています。

// 見やすいように改行を含めていますが、実際には改行は含まれません
{
   "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.xmldependencyresteasy-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