JsonApi4j automatically converts exceptions into JSON:API error responses. Every exception thrown during request processing is caught, mapped to an ErrorsDoc, and returned with the appropriate HTTP status code. No manual error formatting is needed.

How It Works

When an exception is thrown during request processing:

  1. The dispatcher servlet catches the exception
  2. The ErrorHandlerFactoriesRegistry finds a matching ErrorsDocSupplier for the exception class
  3. The supplier converts the exception into an ErrorsDoc (containing one or more ErrorObjects) and an HTTP status code
  4. The servlet writes the JSON:API error response

If no handler matches the exact exception class, the registry searches all registered exception classes and selects the most specific ancestor (closest parent in the class hierarchy). If nothing matches, a generic 500 Internal Server Error is returned.

Exception thrown
    │
    ▼
ErrorHandlerFactoriesRegistry.getErrorResponseMapper(exception)
    │
    ├── Exact class match found? → Use that handler
    │
    ├── Ancestor match found? → Use the most specific ancestor handler
    │
    └── No match → 500 Internal Server Error
    │
    ▼
ErrorsDocSupplier.getErrorResponse(exception) → ErrorsDoc
ErrorsDocSupplier.getHttpStatus(exception) → HTTP status code
    │
    ▼
JSON:API error response written to client

4xx errors are logged at WARN level (client errors). 5xx errors are logged at ERROR level with full stack trace.

Error Response Structure

Every error response follows the JSON:API error format. The response body is an ErrorsDoc containing a list of ErrorObjects:

{
  "errors": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "status": "400",
      "code": "VALUE_IS_ABSENT",
      "detail": "Field 'name' must not be null",
      "source": {
        "parameter": "name"
      }
    }
  ]
}

Each ErrorObject has the following fields:

Field Type Description
id String Unique identifier (UUID) for this error occurrence
status String HTTP status code as a string (e.g., "404")
code String Application-specific error code (e.g., "NOT_FOUND")
detail String Human-readable explanation of the error
source Object Points to the error source — contains pointer, parameter, or header
title String Short summary of the error (optional)
links Object Links related to the error (optional)
meta Object Additional metadata (optional)

Built-in Exception Handling

JsonApi4j registers two default error handler factories.

DefaultErrorHandlerFactory

Covers all framework-specific exceptions from the core and REST layers, as well as user-facing exceptions from the pro.api4.jsonapi4j.exception package. The registry matches the most specific exception class first — subclass handlers take priority over the JsonApi4jException catch-all.

Exception HTTP Status Error Code When It Occurs
JsonApiRequestValidationException 400 From exception Single validation failure — produces one error object
CompositeJsonApiRequestValidationException 400 From errors Multiple validation failures collected by JsonApiRequestValidator — produces multiple error objects in one response
ResourceNotFoundException 404 NOT_FOUND Resource with the given ID does not exist
JsonApi4jException From exception From exception Catch-all for exceptions that extend JsonApi4jException — uses the exception’s own httpStatus and errorCode
DataRetrievalException 502 BAD_GATEWAY Downstream service failed to return data
MappingException 500 INTERNAL_SERVER_ERROR DTO-to-JSON:API object failure
OperationNotFoundException 404 NOT_FOUND Requested operation is not implemented for this resource

Collect-All-Errors Validation

When using JsonApiRequestValidator.forRequest(request), the validator runs all configured validators (path, parameters, headers, body) and collects errors rather than stopping at the first failure. If multiple validators fail, the response contains all errors at once:

{
  "errors": [
    {
      "id": "...",
      "status": "400",
      "code": "VALUE_IS_ABSENT",
      "detail": "value can't be null",
      "source": { "pointer": "/data/attributes" }
    },
    {
      "id": "...",
      "status": "400",
      "code": "INVALID_ENUM_VALUE",
      "detail": "'wrong' value is not allowed, available values: [countries]",
      "source": { "pointer": "/data/relationships/citizenships/data/0/type" }
    }
  ]
}

Each developer-provided validator lambda is atomic — if a lambda throws, the error is collected and the next validator runs. Errors within a single lambda are still fail-fast (e.g., a null check followed by a method call on the same value). This design prevents NPEs from interdependent checks while still collecting errors across independent validators.

Error Codes

Error codes are represented by the ErrorCode interface (single method: toCode()). The framework provides DefaultErrorCodes with 32 built-in codes organized by category:

Request validation: GENERIC_REQUEST_ERROR, MISSING_REQUIRED_PARAMETER, MISSING_REQUIRED_HEADER, INVALID_ENUM_VALUE, VALUE_IS_ABSENT, VALUE_EMPTY, VALUE_TOO_SHORT, VALUE_TOO_LONG, VALUE_TOO_HIGH, VALUE_TOO_LOW, VALUE_INVALID_FORMAT, ARRAY_LENGTH_TOO_SHORT, ARRAY_LENGTH_TOO_LONG, CONFLICTING_PARAMETERS, INVALID_CURSOR, INVALID_LIMIT, INVALID_PAYLOAD

HTTP/server: NOT_FOUND, METHOD_NOT_SUPPORTED, NOT_ACCEPTABLE, UNSUPPORTED_MEDIA_TYPE, CONFLICT, BAD_GATEWAY, INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE, MAX_AMOUNT_OF_RESOURCES

Authentication/authorization: UNAUTHORIZED, ACCESS_TOKEN_REVOKED, ACCESS_TOKEN_EXPIRED, FORBIDDEN, INSUFFICIENT_SCOPES, INSUFFICIENT_ACCESS_TIER

You can define your own error codes by implementing ErrorCode:

public enum MyErrorCodes implements ErrorCode {

    DUPLICATE_EMAIL,
    ACCOUNT_SUSPENDED,
    QUOTA_EXCEEDED;

    @Override
    public String toCode() {
        return name();
    }
}

Custom Error Handlers

To handle your own exceptions, implement ErrorHandlerFactory and register it as a bean. The framework auto-discovers custom factories and adds them to the registry alongside the built-in ones.

1. Define Your Exception

public class DuplicateEmailException extends RuntimeException {

    private final String email;

    public DuplicateEmailException(String email) {
        super("Email already registered: " + email);
        this.email = email;
    }

    public String getEmail() {
        return email;
    }
}

2. Create an ErrorHandlerFactory

public class MyErrorHandlerFactory implements ErrorHandlerFactory {

    @Override
    public Map<Class<? extends Throwable>, ErrorsDocSupplier<?>> getErrorResponseMappers() {
        return Map.of(
            DuplicateEmailException.class, new ErrorsDocSupplier<DuplicateEmailException>() {
                @Override
                public ErrorsDoc getErrorResponse(DuplicateEmailException ex) {
                    return ErrorsDocFactory.conflictErrorsDoc(
                        "Email " + ex.getEmail() + " is already registered"
                    );
                }

                @Override
                public int getHttpStatus(DuplicateEmailException ex) {
                    return 409;
                }
            }
        );
    }
}

ErrorsDocFactory provides convenience methods for common HTTP statuses: badRequestErrorsDoc(), resourceNotFoundErrorsDoc(), conflictErrorsDoc(), badGatewayErrorsDoc(), internalServerErrorsDoc(), unsupportedMediaTypeErrorsDoc(), notAcceptableErrorsDoc(), methodNotSupportedErrorsDoc(), and the generic genericErrorsDoc(status, code, detail).

3. Register It

Define the factory as a Spring bean. The framework auto-discovers all ErrorHandlerFactory beans via ObjectProvider.

@Configuration
public class ErrorConfig {

    @Bean
    public ErrorHandlerFactory myErrorHandlerFactory() {
        return new MyErrorHandlerFactory();
    }
}

Provide the factory as a CDI bean. The framework discovers all ErrorHandlerFactory instances via Instance<ErrorHandlerFactory>.

public class ErrorConfig {

    @Produces
    @Singleton
    public ErrorHandlerFactory myErrorHandlerFactory() {
        return new MyErrorHandlerFactory();
    }
}

Register the factory on the ErrorHandlerFactoriesRegistry before the framework initializes, or set it as a ServletContext attribute.

ErrorHandlerFactoriesRegistry registry = new JsonApi4jErrorHandlerFactoriesRegistry();
registry.registerAll(new DefaultErrorHandlerFactory());
registry.registerAll(new MyErrorHandlerFactory());
servletContext.setAttribute(
    JsonApi4jServletContainerInitializer.ERROR_HANDLER_FACTORIES_REGISTRY_ATT_NAME,
    registry
);

Custom factories are registered after the built-in ones. If you register a handler for an exception class that already has a built-in handler, your handler replaces it. When no exact match exists, the registry selects the most specific registered ancestor — so a custom handler for a parent exception class won’t shadow a more specific built-in handler (or vice versa).

Throwing Errors from Operations

The simplest way to return an error from your operation code is to throw a JsonApi4jException with the appropriate status, code, and detail:

@Override
public UserDto readById(JsonApiRequest request) {
    UserDto user = userDb.findById(request.getResourceId());
    if (user == null) {
        throw new ResourceNotFoundException(
            "users", request.getResourceId()
        );
    }
    if (user.isSuspended()) {
        throw new JsonApi4jException(
            403, MyErrorCodes.ACCOUNT_SUSPENDED,
            "Account " + request.getResourceId() + " is suspended"
        );
    }
    return user;
}

For validation errors, use JsonApiRequestValidator.forRequest(request) with the fluent assertion API. The builder handles error sources automatically for path, parameter, and body field validators:

@Override
public void validate(JsonApiRequest request) {
    forRequest(request)
            .parameters(params -> params
                    .withFilterValidator("region", regions ->
                            regions.ifPresent().allSatisfy(region ->
                                    Validate.assertThat(region).isOneOf(SUPPORTED_REGIONS))))
            .validate();
}

You can also throw JsonApiRequestValidationException directly with an error code, detail message, and optionally an error source. If you don’t need a specific error code, use the single-argument constructor — it defaults to GENERIC_REQUEST_ERROR:

throw new JsonApiRequestValidationException("name must not be blank");

For situations where a resource is not found — either throw ResourceNotFoundException directly or use the exists(predicate) assertion which throws it automatically when the predicate fails:

// Standalone
assertThat(request.getResourceId()).exists(id -> userDb.readById(id) != null);

// Inside a body validator
.withResourceIdValidator(id -> id.exists(resourceId -> userDb.readById(resourceId) != null))

Exception Hierarchy

RuntimeException
│
├── JsonApi4jException (httpStatus, errorCode, detail)        — user-facing
│   ├── JsonApiRequestValidationException (+ source)          — 400 (single error)
│   │   ├── InvalidCursorException                            — 400 INVALID_CURSOR
│   │   ├── InvalidLimitException                             — 400 INVALID_LIMIT
│   │   └── InvalidPayloadException                           — 400 INVALID_PAYLOAD
│   ├── ResourceNotFoundException                             — 404 NOT_FOUND
│   ├── MethodNotSupportedException                           — 405 METHOD_NOT_SUPPORTED
│   ├── NotAcceptableException                                — 406 NOT_ACCEPTABLE
│   └── UnsupportedMediaTypeException                         — 415 UNSUPPORTED_MEDIA_TYPE
│
├── CompositeJsonApiRequestValidationException                — 400 (multiple errors)
│   └── (collected by JsonApiRequestValidator)
│
├── DataRetrievalException                                    — framework-internal
│   └── (caught and mapped to 502 BAD_GATEWAY)
├── MappingException                                          — 500 INTERNAL_SERVER_ERROR
└── OperationNotFoundException                                — 404 NOT_FOUND

User-facing exceptions (extend JsonApi4jException) are designed to be thrown from your operation code. They carry their own HTTP status and error code, so the framework converts them into proper JSON:API error responses automatically.

Framework-internal exceptions (DataRetrievalException, MappingException, OperationNotFoundException) are thrown by the framework itself and handled by DefaultErrorHandlerFactory with dedicated handlers. You generally don’t throw these directly, except DataRetrievalException when a downstream service call fails.