Skip to content

malczuuu/problem4j-spring

Repository files navigation

Problem4J Spring

Build Status Sonatype License Spring Boot

Designing clear and consistent error responses in a REST API is often harder than it looks. Without a shared standard, each application ends up inventing its own ad-hoc format, which quickly leads to inconsistency and confusion. RFC 7807 - Problem Details for HTTP APIs solves this by defining a simple, extensible JSON structure for error messages.

Problem4J brings this specification into the Spring ecosystem, offering a practical way to model, throw, and handle API errors using Problem objects. It helps you enforce a consistent error contract across your services, while staying flexible enough for custom exceptions and business-specific details.

Table of Contents

Why bother with Problem4J

Even though Spring provides ProblemDetail and ErrorResponseException for RFC 7807-compliant error responses, they have different approach than this library offers. It resolves around throwing ErrorResponseException (or any exception that extends from it) or returning ProblemDetail in @ExceptionHandler methods for handlers of individual exceptions. Spring Boot includes some default exception handlers in ResponseEntityExceptionHandler, but that exceptions usually return exact getMessage() in detail field which may leak framework-internals to client applications.

In contrast, Problem4J was created to:

  • Provide a fully immutable, fluent Problem model with support for extensions.
  • Support declarative exception mapping via @ProblemMapping or programmatic one via ProblemException (as a base class) and ProblemResolver (as a library-specific way to add Exception-to-Problem transformations).
  • Interpolate exception fields and context metadata (e.g., context.traceId) if using declarative approach.
  • Offer consistent error responses across WebMVC and WebFlux, including validation and framework exceptions.
  • Allow custom extensions without boilerplate, making structured errors easier to trace and consume.
  • Configure painlessly thanks to Spring Boot autoconfiguration.
  • Provide a predefined set of @RestControllerAdvice implementations to override default Spring Boot responses, so framework details (such as full exception messages) are not leaked to client applications.
  • Include support for built-in ErrorResponseException for compatibility.
  • Integrate seamlessly with existing Spring Boot applications, by possibility to enable selected components only (via @ConditionalOnMissingBean or application properties).

Problem4J is designed for robust, traceable, and fully configurable REST API errors.

Usage

Extensive usage manual explaining library features can be found on repository wiki pages.

The primary ways to produce a Problem response are:

  1. Throwing a ProblemException with a manually built Problem.
  2. Annotating an exception class with @ProblemMapping.
  3. Implementing a custom ProblemResolver.

1. Throwing a ProblemException

throw new ProblemException(
    Problem.builder()
        .type("errors/invalid-request")
        .title("Invalid Request")
        .status(400)
        .detail("not a valid json")
        .build());

It would produce following response with application/problem+json.

{
    "type": "errors/invalid-request",
    "title": "Invalid Request",
    "status": 400,
    "detail": "not a valid json"
}

2. Using @ProblemMapping on a custom exception

@ProblemMapping(
    type = "errors/invalid-request",
    title = "Invalid Request",
    status = 400,
    detail = "{message}: {fieldName}",
    extensions = {"userId", "fieldName"})
public class ExampleException extends RuntimeException {

  private final String userId;
  private final String fieldName;

  public ExampleException(String userId, String fieldName) {
    super("bad input for user " + userId);
    this.userId = userId;
    this.fieldName = fieldName;
  }
}

It would produce following response with application/problem+json.

{
    "type": "errors/invalid-request",
    "title": "Invalid Request",
    "status": 400,
    "detail": "bad input for user u-123: age",
    "userId": "u-123",
    "fieldName": "age"
}

3. Implementing a custom ProblemResolver

@Component
public class ExampleExceptionResolver implements ProblemResolver {

  @Override
  public Class<? extends Exception> getExceptionClass() {
    return ExampleException.class;
  }

  @Override
  public ProblemBuilder resolveBuilder(
      ProblemContext context, Exception ex, HttpHeaders headers, HttpStatusCode status) {
    return Problem.builder()
        .type("errors/invalid-request")
        .title("Invalid Request")
        .status(400)
        .detail("bad input for user " + ((ExampleException) ex).getUserId())
        .extension("userId", ((ExampleException) ex).getUserId());
  }
}

It would produce following response with application/problem+json.

{
    "type": "errors/invalid-request",
    "title": "Invalid Request",
    "status": 400,
    "detail": "bad input for user u-456",
    "userId": "u-456"
}

Maven Dependency

Add library as dependency to Maven or Gradle. See the actual versions on Maven Central. Add it along with repository in your dependency manager. Java 17 or higher is required to use this library.

It's assumed that Spring Boot will be already present in your project dependencies, as problem4j-spring is only an extension to it and does not bring it transitively.

  1. Maven:
    <dependencies>
        <!-- pick the one for your project -->
        <dependency>
            <groupId>io.github.malczuuu.problem4j</groupId>
            <artifactId>problem4j-spring-webflux</artifactId>
            <version>${version}</version>
        </dependency>
        <dependency>
            <groupId>io.github.malczuuu.problem4j</groupId>
            <artifactId>problem4j-spring-webmvc</artifactId>
            <version>${version}</version>
        </dependency>
    </dependencies>
  2. Gradle (Kotlin DSL):
    dependencies {
        // pick the one for your project
        implementation("io.github.malczuuu.problem4j:problem4j-spring-webflux:${version}")
        implementation("io.github.malczuuu.problem4j:problem4j-spring-webmvc:${version}")
    }

Repository

This repository maintains two major versions, supporting Spring Boot 3 and 4. The goal is to maintain both versions at least until Spring Boot 3 reaches its end of life or becomes irrelevant.

branch info latest
main version 2.x supporting Spring Boot 4.x Sonatype
release-v1.*.x version 1.x supporting Spring Boot 3.x Sonatype

Problem4J Links

  • problem4j-core - Core library defining Problem model and ProblemException.
  • problem4j-jackson - Jackson module for serializing and deserializing Problem objects.
  • problem4j-spring - Spring modules extending ResponseEntityExceptionHandler for handling exceptions and returning Problem responses.

Building from source

Expand...

Gradle 9.x+ requires Java 17+ to run, but higher Java versions can also be used. All modules of this project are compiled using a Java 17 toolchain, so the produced artifacts are compatible with Java 17, regardless of the Java version Gradle runs on.

./gradlew clean build

To format the code according to the style defined in build.gradle.kts rules use spotlessApply task.

./gradlew spotlessApply

To publish the built artifacts to local Maven repository, run following command, replacing XXXX with the desired version. By default, the version is derived from git commit hash.

./gradlew -Pversion=XXXX clean build publishToMavenLocal