← Java articles

Java notes

API Evolution: Keep Wire Contracts Stable for Java Clients

Tolerant readers, batch error handling, and golden-file tests

Shubham Anandani··~9 min read

What counts as the contract

  • Field names, types, and optionality.
  • HTTP status codes and which situations produce them.
  • The error envelope: its fields and their meaning.
  • Batch semantics: partial failures, ordering, and duplicates.
  • Behavior when a request omits optional parameters, such as projections.

Be a tolerant reader

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.JsonNode;

@JsonIgnoreProperties(ignoreUnknown = true)
public record ErrorEnvelope(int status, String message, String code, JsonNode errorDetails) {

    public ErrorEnvelope {
        message = message == null ? "" : message;
    }
}
  • ignoreUnknown = true protects you from new server fields.
  • errorDetails stays a raw JsonNode. If the server's details are free-form, do not force them into a fixed class. Keep them untyped, and let the caller interpret them.
  • Missing message becomes an empty string, so downstream code does not need null checks.

Classify errors in one place

public enum ErrorKind { CLIENT_ERROR, NOT_FOUND, UNPROCESSABLE, SERVER_ERROR, UNKNOWN }

public static ErrorKind classify(int status) {
    if (status == 404) return ErrorKind.NOT_FOUND;
    if (status == 422) return ErrorKind.UNPROCESSABLE;
    if (status >= 400 && status < 500) return ErrorKind.CLIENT_ERROR;
    if (status >= 500 && status < 600) return ErrorKind.SERVER_ERROR;
    return ErrorKind.UNKNOWN;
}

Handle batch responses carefully

public static <V> BatchOutcome<V> split(Map<String, V> results, Map<String, ErrorEnvelope> errors) {
    Map<String, V> ok = new LinkedHashMap<>(results);
    Map<String, ErrorKind> failed = new LinkedHashMap<>();
    errors.forEach((id, env) -> {
        ok.remove(id);
        failed.put(id, classify(env.status()));
    });
    return new BatchOutcome<>(ok, failed);
}

public record BatchOutcome<V>(Map<String, V> succeeded, Map<String, ErrorKind> failed) { }

Server-side rules for safe evolution

  • Add fields as optional. Never remove or repurpose a field without a deprecation period.
  • Do not change status codes for existing situations.
  • Do not change the shape of the error envelope. Add to it instead.
  • Be careful with requests that send no projection or field selection. Defaults are part of the contract.
  • Version deliberately when you must break something, and announce it.

Golden-file tests

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ErrorEnvelopeTest {

    private final ObjectMapper mapper = new ObjectMapper();

    @Test
    void parsesCurrentShape() throws Exception {
        String json = """
            {"status":422,"message":"invalid","code":"VALIDATION","errorDetails":{"field":"email"}}
            """;
        ErrorEnvelope env = mapper.readValue(json, ErrorEnvelope.class);
        assertEquals(422, env.status());
        assertEquals("email", env.errorDetails().get("field").asText());
    }

    @Test
    void ignoresFieldsAddedLater() throws Exception {
        String json = """
            {"status":404,"message":"gone","futureField":{"x":1}}
            """;
        assertEquals(404, mapper.readValue(json, ErrorEnvelope.class).status());
    }

    @Test
    void missingOptionalFieldsDoNotFail() throws Exception {
        ErrorEnvelope env = mapper.readValue("{\"status\":500}", ErrorEnvelope.class);
        assertEquals("", env.message());
        assertNull(env.code());
    }
}

Failure modes

  • A status change (for example 400 to 422) with no announcement.
  • A strict client parser that rejects new fields.
  • Error details forced into a typed structure that drops data.
  • Batch handlers that fail the whole batch or hide partial failures.
  • Requests without projections returning a different shape than before.

Demo to build

Takeaways

  • The contract includes errors, status codes, and batch behavior.
  • Clients should be tolerant readers and keep free-form details untyped.
  • Golden-file tests turn accidental contract changes into visible diffs.