1. Context

Java library for working with RFC 9457 standardized Belgif problems.

With this library, RFC 9457 Problems can be treated as standard java exceptions:

  • the server side can throw io.github.belgif.rest.problem.api.Problem exceptions (or subclasses), which are transparently converted to RFC 9457 compliant "application/problem+json" responses

  • the client side can catch io.github.belgif.rest.problem.api.Problem exceptions (or subclasses), which are transparently thrown when an RFC 9457 compliant "application/problem+json" response is received

The library consists of these modules:

2. Release notes

2.1. Version 0.6

belgif-rest-problem-validator:

  • Fix validation for "overflow" SSINs

2.2. Version 0.5

belgif-rest-problem-spring:

Split into belgif-rest-problem-spring-boot-2 and belgif-rest-problem-spring-boot-3. To benefit from Spring Boot 2.x or 3.x specific features, replace dependencies to belgif-rest-problem-spring by the version-specific variant.

belgif-rest-problem-spring-boot-3:

  • Map NoResourceFoundException to 404 urn:problem-type:belgif:resourceNotFound

  • Added support for RestClient API

2.3. Version 0.4

belgif-rest-problem:

  • Removed deprecated InvalidParamProblem: use InputValidationProblem, which supports both the legacy invalidParams[] and the new issues[] structure

  • Replace specific urn:problem-type:cbss:input-validation:unknownSsin issue type by generic urn:problem-type:cbss:input-validation:referencedResourceNotFound

  • Added optional replacedByHref property to replacedSsin issue type

belgif-rest-problem-validator:

  • Added requireIfPresent check for validating input(s) that must be present when a given target input is present

2.4. Version 0.3

belgif-rest-problem:

  • Removed deprecated "status" and "instance" properties from InputValidationIssue

belgif-rest-problem-spring:

  • Added support for bean validation

  • Added Jakarta EE 9/10 support: use <classifier>jakarta</classifier>

2.5. Version 0.2

belgif-rest-problem:

  • Extracted RequestValidator to separate belgif-rest-problem-validator module.

  • Added equals() and hashCode() to all Problem classes

  • Move additionalProperties from DefaultProblem to Problem

belgif-rest-problem-java-ee:

  • Added Jakarta EE 9/10 support: use <classifier>jakarta</classifier>

belgif-rest-problem-spring:

  • Remove be.fgov.kszbcss from default scanned problem type packages

  • Rename io.github.belgif.rest.problem.spring.scan-additional-problem-packages configuration property to io.github.belgif.rest.problem.scan-additional-problem-packages

documentation:

2.6. Version 0.1

Initial release under Belgif organization.

3. belgif-rest-problem

The Belgif REST Guidelines define a standard error handling approach, based on RFC 9457 "Problem Details for HTTP APIs".

The belgif-rest-problem library implements these standardized problem types as an exception hierarchy, with an abstract io.github.belgif.rest.problem.api.Problem base class (containing the standardized "type", "href", "title", "status", "detail" and "instance" properties).

Problem
├── ClientProblem (4xx)
|   └── InputValidationProblem 1..* InputValidationIssue
├── ServerProblem (5xx)
└── DefaultProblem (fallback)

3.1. Belgif problem types

The package io.github.belgif.rest.problem contains all problem types defined in the Belgif REST Guidelines.

Problem
├── ClientProblem (4xx)
|   ├── InputValidationProblem 1..* InputValidationIssue
|   |   ├── BadRequestProblem (400)
|   |   ├── MissingPermissionProblem (403)
|   |   └── ResourceNotFoundProblem (404)
|   ├── ExpiredAccessTokenProblem (401)
|   ├── InvalidAccessTokenProblem (401)
|   ├── NoAccessTokenProblem (401)
|   ├── MissingScopeProblem (403)
|   ├── PayloadTooLargeProblem (413)
|   ├── TooManyRequestsProblem (429)
|   └── TooManyFailedRequestsProblem (429)
└── ServerProblem (5xx)
    ├── InternalServerErrorProblem (500)
    ├── BadGatewayProblem (502)
    └── ServiceUnavailableProblem (503)
  • The BadRequestProblem is backwards compatible with the deprecated InvalidParamProblem (with invalidParams[] instead of issues[]) of openapi-problem 1.1.

  • Besides the standard in/name/value properties for referencing a single input, the InputValidationIssue class also supports an inputs[] array for referencing multiple in/name/value inputs. This is not standardized.

  • MissingPermissionProblem extending InputValidationProblem is not standardized.

3.2. Custom problem types

Besides the standardized Belgif problem types, custom problem types can also be defined.

The naming convention should be respected for custom problem type URIs:

  • urn:problem-type:<org>:<type> for organization-wide problem types

  • urn:problem-type:<org>:<api>:<type> for API-local problem types

Implementing an API-local custom problem type
@ProblemType(TooManyResultsProblem.TYPE)
public static class TooManyResultsProblem extends ClientProblem {

    public static final String TYPE = "urn:problem-type:cbss:legallog:tooManyResults";
    public static final URI TYPE_URI = URI.create(TYPE);
    public static final URI HREF =
            URI.create("https://api.ksz-bcss.fgov.be/legallog/v1/refData/problemTypes/" + TYPE);
    public static final String TITLE = "Too Many Results";
    public static final int STATUS = 400;

    private static final long serialVersionUID = 1L;

    public TooManyResultsProblem() {
        super(TYPE_URI, HREF, TITLE, STATUS);
        setDetail("Result's number of global transactions exceeds the limit");
    }

    public TooManyResultsProblem(int limit) {
        this();
        setDetail("Result's number of global transactions exceeds the limit of " + limit);
    }

}

Your custom problem type can define additional properties if necessary. For problem types related to input parameters, you can extend from InputValidationProblem instead of Problem.

Don’t forget to add the @ProblemType annotation.

3.3. @ProblemType and ProblemTypeRegistry

We make use of Jackson’s polymorphic deserialization feature, so the Jackson ObjectMapper knows which problem type to instantiate when deserializing a problem. The problem subclasses have their problem type URI configured in the @ProblemType annotation.

These are discovered automagically by a ProblemTypeRegistry implementation.

  • in a Java EE container, this is done by the CdiProblemTypeRegistry CDI extension, integrated with the Jackson ObjectMapper by the CdiProblemModule.

  • in a Spring Boot container, this is done by the SpringProblemTypeRegistry, integrated with the Jackson ObjectMapper by the SpringProblemModule.

4. belgif-rest-problem-validator

The io.github.belgif.rest.problem.validation.RequestValidator can be used to perform validations on the input parameters of an API request. This validation does not stop on the first invalid input. It performs all configured validations and if any of them failed, a BadRequestProblem is thrown, containing each encountered InputValidationIssue.

RequestValidator
// example for an API resource with query parameters:
// ssin, enterpriseNumber, startDate, endDate and password
new RequestValidator()
        .ssin(Input.query("ssin", ssin)) (1)
        .enterpriseNumber(Input.query("enterpriseNumber", enterpriseNumber)) (2)
        .period(Input.query("startDate", startDate), Input.query("endDate", endDate)) (3)
        .exactlyOneOf(Input.query("ssin", ssin),
                Input.query("enterpriseNumber", enterpriseNumber)) (4)
        .custom(() -> { (5)
            // custom validation logic returning Optional<InputValidationIssue>
            if (!"secret".equals(password)) {
                return Optional.of(new InputValidationIssue(InEnum.QUERY, "password", password)
                        .type("urn:problem-type:cbss:input-validation:example:invalidPassword")
                        .title("Invalid password"));
            }
            return Optional.empty();
        })
        .validate(); (6)
1 validate SSIN structure
2 validate enterprise number structure
3 validate endDate >= startDate
4 cross-parameter validation: either ssin or enterprise number should be present, but not both
5 custom validations are also supported
6 perform the configured validations, throws BadRequestProblem if any validations failed
Example BadRequestProblem for invalid request /resource?ssin=00000000000&enterpriseNumber=1111111111&startDate=2023-12-31&endDate=2023-01-01&password=oops
{
  "type": "urn:problem-type:belgif:badRequest",
  "href": "https://www.belgif.be/specification/rest/api-guide/problems/badRequest.html",
  "title": "Bad Request",
  "status": 400,
  "detail": "The input message is incorrect",
  "issues": [
    {
      "type": "urn:problem-type:cbss:input-validation:invalidStructure",
      "title": "Input value has invalid structure",
      "detail": "SSIN 11111111111 is invalid",
      "in": "query",
      "name": "ssin",
      "value": "11111111111"
    },
    {
      "type": "urn:problem-type:cbss:input-validation:invalidStructure",
      "title": "Input value has invalid structure",
      "detail": "Enterprise number 2222222222 is invalid",
      "in": "query",
      "name": "enterpriseNumber",
      "value": "2222222222"
    },
    {
      "type": "urn:problem-type:cbss:input-validation:invalidPeriod",
      "title": "Period is invalid",
      "detail": "endDate should not preceed startDate",
      "inputs": [
        {
          "in": "query",
          "name": "startDate",
          "value": "2023-12-31"
        },
        {
          "in": "query",
          "name": "endDate",
          "value": "2023-01-01"
        }
      ]
    },
    {
      "type": "urn:problem-type:cbss:input-validation:exactlyOneOfExpected",
      "title": "Exactly one of these inputs must be present",
      "detail": "Exactly one of these inputs must be present: ssin, enterpriseNumber",
      "inputs": [
        {
          "in": "query",
          "name": "ssin",
          "value": "11111111111"
        },
        {
          "in": "query",
          "name": "enterpriseNumber",
          "value": "2222222222"
        }
      ]
    },
    {
      "type": "urn:problem-type:cbss:input-validation:example:invalidPassword",
      "title": "Invalid password",
      "in": "query",
      "name": "password",
      "value": "oops"
    }
  ]
}

5. belgif-rest-problem-java-ee

This module provides components that handle Java EE / Jakarta EE integration with the belgif-rest-problem library:

  • CdiProblemTypeRegistry: ProblemTypeRegistry implementation that uses CDI component scanning to detect @ProblemType annotations.

  • CdiProblemModule: a Jackson Module that registers the CdiProblemTypeRegistry.

  • ProblemObjectMapperContextResolver: a JAX-RS ContextResolver for a Jackson ObjectMapper with the CdiProblemModule registered. Its priority of "Priorities.USER + 200" allows it to be overridden if needed by the client application.

  • ConstraintViolationExceptionMapper: a JAX-RS ExceptionMapper that converts ConstraintViolationException to an HTTP 400 BadRequestProblem.

  • ProblemExceptionMapper: a JAX-RS ExceptionMapper that converts Problem exceptions to a proper application/problem+json response.

  • WebApplicationExceptionMapper: a JAX-RS ExceptionMapper that handles WebApplicationExceptions thrown by the JAX-RS runtime itself, to prevent them from being handled by the DefaultExceptionMapper.

  • DefaultExceptionMapper: a JAX-RS ExceptionMapper that converts any other uncaught exception to an HTTP 500 InternalServerErrorProblem.

  • JAX-RS Client integration:

    • ProblemClientResponseFilter: JAX-RS ClientResponseFilter that converts problem response to a ProblemWrapper exception.

      In accordance with the spec, any exception thrown by a JAX-RS ClientResponseFilter gets wrapped in a ResponseProcessingException, unless the exception itself is a ResponseProcessingException. For that reason, the ProblemClientResponseFilter wraps the Problem exception in a "ProblemWrapper" class that extends ProblemClientResponseFilter.
    • ProblemSupport: for programmatically enabling unwrapping of ProblemWrapper to Problem exceptions on a JAX-RS Client (also works for RESTEasy Proxy Framework clients).

      private Client client = ProblemSupport.enable(ClientBuilder.newClient());
    • ProblemClientBuilderProducer: makes ProblemSupport-enabled ClientBuilder and Client beans available for CDI @Inject

      @Inject
      public void setClient(Client client) {
          this.client = client;
      }
  • MicroProfile REST Client integration:

    • ProblemResponseExceptionMapper: a ResponseExceptionMapper that converts problem responses to Problem exceptions.

    • ProblemRestClientListener: a RestClientListener that registers the ProblemObjectMapperContextResolver and ProblemResponseExceptionMapper.

  • Jakarta EE 9+: the main belgif-rest-problem-java-ee artifact targets Java EE (javax package namespace). A secondary artifact that targets Jakarta EE 9+ (jakarta package namespace) is available with <classifier>jakarta</classifier>.

5.1. Using rest-problem library for implementing REST services

When implementing a REST service with JAX-RS, returning a problem is as simple as throwing the problem exception from your code:

Throwing a problem in a JAX-RS application
throw new BadRequestProblem(
        new InputValidationIssue(InEnum.PATH, "sectorCode", sectorCode)
                .title("Invalid sector code"));

5.2. Using rest-problem library for consuming REST services

When using the MicroProfile REST Client API, handling a problem is as simple as catching the problem exception in your code:

Handling a problem on the client
try {
    service.call();
} catch (BadRequestProblem p) {
    String detail = p.getDetail();
    List<InputValidationIssue> issues = p.getIssues();
    // handle the BadRequestProblem
} catch (ResourceNotFoundProblem p) {
    String detail = p.getDetail();
    List<InputValidationIssue> issues = p.getIssues();
    // handle the ResourceNotFoundProblem
} catch (WebApplicationException e) {
    LOGGER.info(e.getMessage());
    // handle all other WebApplicationException
}
If the service returns a problem type for which you do not have a corresponding @ProblemType on your classpath, a DefaultProblem is used instead.

6. belgif-rest-problem-spring

This module provides auto-configuration for components that handle Spring Boot integration with the belgif-rest-problem library:

  • SpringProblemTypeRegistry: ProblemTypeRegistry implementation that uses classpath scanning to detect @ProblemType annotations. By default, only package io.github.belgif.rest.problem is scanned for @ProblemType annotations. Additional packages to scan can be configured in your application.properties:

io.github.belgif.rest.problem.scan-additional-problem-packages=com.acme.custom
  • SpringProblemModule: a Jackson Module that registers the SpringProblemTypeRegistry.

  • ProblemExceptionHandler: an exception handler for RestControllers that handles the response serialization for Problem exceptions, and converts all other uncaught exceptions to an InternalServerErrorProblem.

  • BeanValidationExceptionsHandler: an exception handler for RestControllers that converts bean validation related exceptions to HTTP 400 BadRequestProblem.

  • RoutingExceptionsHandler: an exception handler for RestControllers that converts routing related validation exceptions to HTTP 400 BadRequestProblem.

  • ProblemResponseErrorHandler: a RestTemplate error handler that converts problem responses to Problem exceptions.

  • ProblemRestTemplateCustomizer: a RestTemplateCustomizer that registers the ProblemResponseErrorHandler.

  • ProblemWebClientCustomizer: a WebClientCustomizer that registers a filter that converts problem responses to Problem exceptions. This handles integration with the Reactive WebClient.

In general, these components make it possible to use standard java exception handling (throw and try-catch) for dealing with problems in Spring Boot REST APIs.

6.1. belgif-rest-problem-spring-boot-2

Rather than depending on belgif-rest-problem-spring directly, Spring Boot 2.x users are recommended to depend on belgif-rest-problem-spring-boot-2, which adds some Spring Boot 2.x specific integrations.

6.2. belgif-rest-problem-spring-boot-3

Rather than depending on belgif-rest-problem-spring directly, Spring Boot 3.x users are recommended to depend on belgif-rest-problem-spring-boot-3, which adds some Spring Boot 3.x specific integrations:

  • NoResourceFoundExceptionHandler: an exception handler for RestControllers that converts NoResourceFoundException to HTTP 404 ResourceNotFoundProblem.

  • ProblemRestClientCustomizer: a RestClientCustomizer that registers the ProblemResponseErrorHandler.

7. Code generators

Code generators should be configured to use the belgif-rest-problem types for problem-related model classes, instead of generating them.

7.1. openapi-generator-maven-plugin

When using openapi-generator-maven-plugin, configure <schemaMappings> as follows:

<schemaMappings>
  Problem=io.github.belgif.rest.problem.api.Problem,
  InputValidationProblem=io.github.belgif.rest.problem.api.InputValidationProblem,
  InputValidationIssue=io.github.belgif.rest.problem.api.InputValidationIssue,
  InvalidParamProblem=io.github.belgif.rest.problem.api.InputValidationProblem,
  InvalidParam=io.github.belgif.rest.problem.api.InvalidParam
</schemaMappings>

7.2. swagger-codegen-maven-plugin

When using swagger-codegen-maven-plugin, configure <importMappings> as follows:

<importMappings>
  Problem=io.github.belgif.rest.problem.api.Problem,
  InputValidationProblem=io.github.belgif.rest.problem.api.InputValidationProblem,
  InputValidationIssue=io.github.belgif.rest.problem.api.InputValidationIssue,
  InvalidParamProblem=io.github.belgif.rest.problem.api.InputValidationProblem,
  InvalidParam=io.github.belgif.rest.problem.api.InvalidParam
</importMappings>