zalando/problem

repository·main·Indexed 21 days ago

https://github.com/zalando/problem

A Java library that implements the RFC 7807 'application/problem+json' standard for expressing errors in REST APIs. It provides a fluent builder API, support for custom problem classes via AbstractThrowableProblem and the Exceptional interface, and dedicated integration modules for Jackson and Gson.

Tokens
2.8K
Snippets
11
Records
12
Agent score
26%

What's inside zalando-problem

  1. Overview of the Problem library

    main
    The problem library provides an implementation of the application/problem+json specification (RFC 7807). It offers an extensible set of interfaces and implementations for expressing errors in REST API implementations. The core library is decoupled from specific JSON libraries, but provides dedicated modules for Jackson and Gson to facilitate easy integration.
  2. Install the Problem library

    main

    Add the problem dependency to your project. If you require JSON serialization support, you should also include the specific module for your preferred JSON library (e.g., problem-jackson3 or problem-gson).

    <dependency>
        <groupId>org.zalando</groupId>
        <artifactId>problem</artifactId>
        <version>${problem.version}</version>
    </dependency>
    <dependency>
        <groupId>org.zalando</groupId>
        <artifactId>problem-jackson3</artifactId>
        <version>${problem.version}</version>
    </dependency>
    <dependency>
        <groupId>org.zalando</groupId>
        <artifactId>problem-gson</artifactId>
        <version>${problem.version}</version>
    </dependency>
  3. Throw problems as exceptions

    main

    To throw a problem as a Java exception, use one of two approaches:

    1. Inherit from AbstractThrowableProblem: This class subclasses RuntimeException, making it a ThrowableProblem automatically.
    2. Implement the Exceptional marker interface: If you have an existing exception hierarchy, implement Exceptional. The Jackson support module will recognize this and handle inherited Throwable properties correctly.
    // Approach 1: Inherit from AbstractThrowableProblem
    public final class OutOfStockProblem extends AbstractThrowableProblem { ... }
    
    // Approach 2: Implement Exceptional on existing exceptions
    public final class OutOfStockProblem extends BusinessException implements Exceptional { ... }
  4. Configure Problem with Java Modules (JPMS)

    main

    The library is fully compatible with the Java Platform Module System. To use it in a modular project, add the following requirements to your module-info.java:

    module org.example {
        requires org.zalando.problem;
        // pick needed dependencies
        requires org.zalando.problem.jackson;
        requires org.zalando.problem.gson;
    }
  5. Deserialize problems with Jackson

    main

    When using Jackson to read problems, you must handle polymorphic deserialization. If you have custom problem types, you must:

    1. Register them as subtypes in the ObjectMapper using registerSubtypes(Class...).
    2. Annotate the custom class with @JsonTypeName.
    3. Annotate the constructor with @JsonCreator.
    // Registering subtypes
    mapper.builder().registerSubtypes(OutOfStockProblem.class);
    
    // Custom class requirements
    @JsonTypeName(OutOfStockProblem.TYPE_VALUE)
    public final class OutOfStockProblem implements Problem {
        @JsonCreator
        public OutOfStockProblem(final String product) { ... }
    }
    
    // Basic reading
    Problem problem = mapper.readValue(..., Problem.class);
  6. Catch and handle specific problems

    main

    If you are reading a problem from a server response, you can catch specific types using instanceof or by deserializing into ThrowableProblem.class (or Exceptional.class if using the marker interface) and then catching the specific subtypes.

    If using Exceptional, you must call .propagate() on the deserialized object to throw it.

    // Using ThrowableProblem
    try {
        throw mapper.readValue(.., ThrowableProblem.class);
    } catch (OutOfStockProblem e) {
        // Handle specific type
    } catch (ThrowableProblem e) {
        // Fallback
    }
    
    // Using Exceptional
    try {
        throw mapper.readValue(.., Exceptional.class).propagate();
    } catch (OutOfStockProblem e) {
        // Handle specific type
    }
  7. Configure stack traces in problems

    main

    By default, stack traces are not serialized to avoid leaking implementation details. To enable them (e.g., for debugging in integration environments), register the ProblemModule with .withStackTraces() in your JsonMapper.

    Warning: Stack traces are not deserializable from JSON by design. To fix the fact that deserialized stack traces often point to the deserialization framework rather than the source, implement the StackTraceProcessor interface and register it via ServiceLoader.

    // Enable stack trace serialization
    JsonMapper mapper = JsonMapper.builder()
            .addModule(new ProblemModule().withStackTraces())
            .build();
    
    // Customizing stack traces via SPI
    public interface StackTraceProcessor {
        Collection<StackTraceElement> process(final Collection<StackTraceElement> elements);
    }
  8. Configure Problem with Jackson

    main

    If you are using Jackson for JSON processing, you must register the ProblemModule with your JsonMapper. You can do this explicitly or by using the Service Provider Interface (SPI) to automatically find and add modules.

    // Explicit registration
    JsonMapper mapper = JsonMapper.builder().addModule(new ProblemModule()).build();
    
    // Using SPI to find and add modules automatically
    JsonMapper mapper = JsonMapper.builder().findAndAddModules().build();
  9. Create generic problems using Status

    main

    For simple cases where an HTTP status code is sufficient, use Problem.valueOf(Status). This creates a problem with an about:blank type and a title matching the recommended HTTP status phrase. You can also provide a custom detail string.

    Example outputs:

    • Problem.valueOf(Status.NOT_FOUND) $\rightarrow$ {"title": "Not Found", "status": 404}
    • Problem.valueOf(Status.SERVICE_UNAVAILABLE, "Database not reachable") $\rightarrow$ {"title": "Service Unavailable", "status": 503, "detail": "Database not reachable"}
    var problem = Problem.valueOf(Status.NOT_FOUND);
    
    var problemWithDetail = Problem.valueOf(Status.SERVICE_UNAVAILABLE, "Database not reachable");
  10. Create problems using the Problem Builder

    main

    Use the fluent Problem.builder() API to construct highly flexible problems without creating custom classes. This allows you to specify type (as a URI), title, status, detail, and custom properties via the .with(key, value) method.

    var problem = Problem.builder()
        .withType(URI.create("https://example.org/out-of-stock"))
        .withTitle("Out of Stock")
        .withStatus(BAD_REQUEST)
        .withDetail("Item B00027Y5QG is no longer available")
        .with("product", "B00027Y5QG")
        .build();
  11. Create causal chains (nested problems)

    main

    You can nest problems using the .withCause(Problem) method in the builder. This follows the standard Java Throwable pattern and produces a cause field in the resulting JSON.

    ThrowableProblem problem = Problem.builder()
        .withType(URI.create("https://example.org/order-failed"))
        .withTitle("Order failed")
        .withStatus(BAD_REQUEST)
        .withCause(Problem.builder()
          .withType(URI.create("https://example.org/out-of-stock"))
          .withTitle("Out of Stock")
          .withStatus(BAD_REQUEST)
          .build())
        .build();
    
    // Access the cause via standard Throwable API
    Problem cause = problem.getCause();
  12. Create custom problem classes

    main

    For reusable and shared problem types, implement the Problem interface or extend AbstractThrowableProblem. Extending AbstractThrowableProblem is recommended if you want the problem to behave like a RuntimeException that can be thrown.

    @Immutable
    public final class OutOfStockProblem extends AbstractThrowableProblem {
    
        static final URI TYPE = URI.create("https://example.org/out-of-stock");
        private final String product;
    
        public OutOfStockProblem(final String product) {
            super(TYPE, "Out of Stock", BAD_REQUEST, format("Item %s is no longer available", product));
            this.product = product;
        }
    
        public String getProduct() {
            return product;
        }
    }
    
    // Usage
    var problem = new OutOfStockProblem("B00027Y5QG");