CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-anthropic--anthropic-java

The Anthropic Java SDK provides convenient access to the Anthropic REST API from applications written in Java

Overview
Eval results
Files

structured-outputs.mddocs/

Structured Outputs

Anthropic Structured Outputs (beta) is a feature that ensures the model will always generate responses that adhere to a supplied JSON schema. The SDK provides automatic JSON schema derivation from Java classes, enabling type-safe structured responses without manual schema definition.

Overview

Structured Outputs guarantees that Claude's responses conform to your specified JSON schema, making it ideal for extracting structured data, generating consistent formats, and building reliable integrations. The SDK automatically converts Java classes into JSON schemas and deserializes responses back into Java objects.

Key features:

  • Automatic JSON schema generation from Java class structure
  • Type-safe response handling with compile-time checking
  • Support for nested classes, collections, and optional fields
  • Jackson and Swagger annotations for schema customization
  • Streaming support with JSON accumulation
  • Local schema validation before API calls

Output Format Configuration

Configure structured outputs by calling outputFormat(Class<T>) on the message builder. This method derives a JSON schema from the provided class and returns a generic builder for type-safe parameter construction.

public <T> StructuredMessageCreateParams.Builder<T> outputFormat(Class<T> outputClass);
public <T> StructuredMessageCreateParams.Builder<T> outputFormat(
    Class<T> outputClass,
    JsonSchemaLocalValidation validation
);

Parameters:

  • outputClass - The Java class to derive the JSON schema from
  • validation - Whether to validate the schema locally (default: JsonSchemaLocalValidation.YES)

Returns: A StructuredMessageCreateParams.Builder<T> for building type-safe parameters

StructuredMessageCreateParams

Generic builder type for creating message requests with structured outputs. When you call outputFormat(Class<T>) on a standard builder, it returns this specialized builder parameterized with your output type.

public final class StructuredMessageCreateParams<T> {
    public static <T> Builder<T> builder();

    public static final class Builder<T> {
        public Builder<T> model(Model model);
        public Builder<T> maxTokens(long maxTokens);
        public Builder<T> messages(List<MessageParam> messages);
        public Builder<T> addMessage(MessageParam message);
        public Builder<T> addUserMessage(String content);
        public Builder<T> addAssistantMessage(String content);
        public Builder<T> temperature(Double temperature);
        public Builder<T> system(String system);
        public Builder<T> tools(List<ToolUnion> tools);
        public Builder<T> addTool(Tool tool);
        public Builder<T> toolChoice(ToolChoice toolChoice);
        public Builder<T> metadata(Metadata metadata);
        public Builder<T> stopSequences(List<String> stopSequences);
        public Builder<T> topK(Long topK);
        public Builder<T> topP(Double topP);
        public StructuredMessageCreateParams<T> build();
    }
}

Defining Java Classes

Classes used for structured outputs must follow specific rules to ensure valid JSON schema generation:

Field Declaration Rules

Public fields are included in the JSON schema by default:

class Person {
    public String name;        // Included
    public int birthYear;      // Included
    private String internal;   // Excluded by default
}

Private fields with public getters are included using the getter method name:

class Person {
    private String name;

    public String getName() {  // Creates "name" property
        return name;
    }
}

Non-conventional getter names require @JsonProperty annotation:

class Person {
    private String name;

    @JsonProperty
    public String fullName() {  // Creates "fullName" property
        return name;
    }
}

Nested Classes and Collections

Classes can contain fields of other class types and use standard Java collections:

class Person {
    public String name;
    public int birthYear;
}

class Book {
    public String title;
    public Person author;              // Nested class
    public int publicationYear;
}

class BookList {
    public List<Book> books;           // Collection of nested classes
}

Supported collection types:

  • List<T>
  • Set<T>
  • Collection<T>
  • Arrays (T[])

Required Properties

Each class must define at least one property. A validation error occurs if:

  • The class has no fields or getter methods
  • All public fields/getters are annotated with @JsonIgnore
  • All non-public fields/getters lack @JsonProperty annotations
  • A field uses Map type (maps have no named properties)

Optional Fields

Use java.util.Optional<T> to represent optional properties. The AI model decides whether to provide a value or leave it empty.

import java.util.Optional;

class Book {
    public String title;                // Required
    public Person author;               // Required
    public int publicationYear;         // Required
    public Optional<String> isbn;       // Optional
    public Optional<String> subtitle;   // Optional
}

Usage:

StructuredMessage<Book> response = client.beta().messages().create(params);
Book book = response.content().get(0).text().text();

// Check optional fields
if (book.isbn.isPresent()) {
    System.out.println("ISBN: " + book.isbn.get());
} else {
    System.out.println("No ISBN provided");
}

JSON Schema Derivation

The SDK automatically generates JSON schemas from Java class structure:

Input class:

class Book {
    public String title;
    public Person author;
    public int publicationYear;
    public Optional<String> isbn;
}

class Person {
    public String name;
    public int birthYear;
}

Generated schema (conceptual):

{
  "type": "object",
  "properties": {
    "title": { "type": "string" },
    "author": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "birthYear": { "type": "integer" }
      },
      "required": ["name", "birthYear"]
    },
    "publicationYear": { "type": "integer" },
    "isbn": { "type": "string" }
  },
  "required": ["title", "author", "publicationYear"]
}

Schema generation rules:

  • Public fields and getter methods become schema properties
  • Field names become property names (using getter naming conventions)
  • Java types map to JSON schema types (String → string, int/long → integer, etc.)
  • Optional<T> fields are not included in required array
  • Nested classes become nested object schemas
  • Collections become array schemas with item types

BetaJsonOutputFormat

Low-level class for manually defining JSON schemas when automatic derivation is not suitable. This provides complete control over schema structure.

public final class BetaJsonOutputFormat {
    public static Builder builder();

    public static final class Builder {
        public Builder type(Type type);
        public Builder schema(JsonSchema schema);
        public Builder build();
    }

    public enum Type {
        JSON_SCHEMA
    }

    public static final class JsonSchema {
        public static Builder builder();

        public static final class Builder {
            public Builder type(String type);
            public Builder properties(Map<String, Object> properties);
            public Builder required(List<String> required);
            public Builder additionalProperties(boolean allowed);
            public JsonSchema build();
        }
    }
}

Manual schema example:

BetaJsonOutputFormat format = BetaJsonOutputFormat.builder()
    .type(BetaJsonOutputFormat.Type.JSON_SCHEMA)
    .schema(BetaJsonOutputFormat.JsonSchema.builder()
        .type("object")
        .properties(Map.of(
            "title", Map.of("type", "string"),
            "year", Map.of("type", "integer")
        ))
        .required(List.of("title", "year"))
        .build())
    .build();

StructuredMessage

Response type for structured output requests. Extends the standard Message class with typed content access methods.

public final class StructuredMessage<T> extends Message {
    @Override
    public List<ContentBlock> content();

    // Type-safe content access
    public T getStructuredContent();
    public Optional<T> getStructuredContentSafe();
}

Usage:

StructuredMessageCreateParams<BookList> params = MessageCreateParams.builder()
    .model(Model.CLAUDE_SONNET_4_5_20250929)
    .maxTokens(2048)
    .outputFormat(BookList.class)
    .addUserMessage("List famous novels.")
    .build();

StructuredMessage<BookList> response = client.beta().messages().create(params);

// Access structured content
response.content().stream()
    .flatMap(block -> block.text().stream())
    .flatMap(textBlock -> textBlock.text().books.stream())
    .forEach(book -> System.out.println(book.title + " by " + book.author.name));

Local Schema Validation

The SDK performs local validation before sending requests to ensure schemas comply with Anthropic's restrictions. This catches errors early and provides detailed feedback.

public enum JsonSchemaLocalValidation {
    YES,
    NO
}

Validation Behavior

Local Validation (default):

  • Validates schema against Anthropic's subset of JSON Schema specification
  • Checks for unsupported data types and constructs
  • Throws exception with detailed error message if validation fails
  • No API request sent if validation fails

Remote Validation:

  • Performed by Anthropic API after receiving request
  • May differ from local validation if SDK version is outdated

When to Disable Local Validation

Disable local validation when:

  • Using newer Anthropic API features not yet supported by SDK version
  • Local validation incorrectly rejects valid schemas (version mismatch)
  • Debugging schema compatibility issues

Disable validation:

StructuredMessageCreateParams<BookList> params = MessageCreateParams.builder()
    .model(Model.CLAUDE_SONNET_4_5_20250929)
    .maxTokens(2048)
    .outputFormat(BookList.class, JsonSchemaLocalValidation.NO)
    .addUserMessage("List famous novels.")
    .build();

Jackson Annotations

Jackson Databind annotations control schema generation and add descriptive metadata.

@JsonClassDescription

Adds a description to a class in the generated schema.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonClassDescription {
    String value();
}

Example:

import com.fasterxml.jackson.annotation.JsonClassDescription;

@JsonClassDescription("The details of one published book")
class Book {
    public String title;
    public Person author;
    public int publicationYear;
}

@JsonPropertyDescription

Adds a description to a field or getter method in the generated schema.

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonPropertyDescription {
    String value();
}

Example:

import com.fasterxml.jackson.annotation.JsonPropertyDescription;

class Person {
    @JsonPropertyDescription("The first name and surname of the person")
    public String name;

    public int birthYear;

    @JsonPropertyDescription("The year the person died, or 'present' if the person is living")
    public String deathYear;
}

@JsonIgnore

Excludes a public field or getter method from the generated schema.

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonIgnore {
    boolean value() default true;
}

Example:

import com.fasterxml.jackson.annotation.JsonIgnore;

class Book {
    public String title;
    public Person author;
    public int publicationYear;

    @JsonIgnore
    public String internalNotes;  // Excluded from schema
}

@JsonProperty

Includes a non-public field or getter method in the generated schema, or customizes property names.

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonProperty {
    String value() default "";
    boolean required() default true;
}

Example:

import com.fasterxml.jackson.annotation.JsonProperty;

class Book {
    @JsonProperty
    private String title;  // Included despite being private

    private String author;

    @JsonProperty("author_name")
    public String getAuthor() {  // Property named "author_name"
        return author;
    }
}

Note: The required attribute is ignored. Anthropic schemas require all properties to be marked as required (unless wrapped in Optional<T>).

Swagger Annotations

Swagger/OpenAPI annotations add type-specific constraints to schema properties.

@Schema

Adds constraints and metadata to fields and classes.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Schema {
    String description() default "";
    String format() default "";
    String minimum() default "";
    String maximum() default "";
    String pattern() default "";
    int minLength() default Integer.MIN_VALUE;
    int maxLength() default Integer.MAX_VALUE;
}

Supported constraints:

  • description - Property or class description
  • format - String format (e.g., "date", "date-time", "email", "uri")
  • minimum - Minimum numeric value (as string)
  • maximum - Maximum numeric value (as string)
  • pattern - Regular expression pattern for strings
  • minLength - Minimum string length
  • maxLength - Maximum string length

Example:

import io.swagger.v3.oas.annotations.media.Schema;

class Article {
    public String title;

    @Schema(format = "date")
    public String publicationDate;

    @Schema(minimum = "1", maximum = "10000")
    public int pageCount;

    @Schema(pattern = "^[A-Z]{3}-\\d{4}$")
    public String articleCode;

    @Schema(minLength = 10, maxLength = 500)
    public String summary;
}

@ArraySchema

Adds array-specific constraints to collection fields.

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ArraySchema {
    int minItems() default Integer.MIN_VALUE;
    int maxItems() default Integer.MAX_VALUE;
    boolean uniqueItems() default false;
}

Example:

import io.swagger.v3.oas.annotations.media.ArraySchema;

class Article {
    @ArraySchema(minItems = 1, maxItems = 10)
    public List<String> authors;

    @ArraySchema(uniqueItems = true)
    public List<String> tags;

    public String title;
}

Annotation precedence: When both Jackson and Swagger annotations set the same schema field, Jackson annotations take precedence. For example:

import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;

class MyObject {
    @Schema(description = "Swagger description")
    @JsonPropertyDescription("Jackson description")
    public String myProperty;  // Description will be "Jackson description"
}

Streaming Integration

Structured outputs work with streaming by accumulating JSON strings from stream events and deserializing them into Java objects after the stream completes.

BetaMessageAccumulator

Helper class for accumulating streaming events into a complete message.

public final class BetaMessageAccumulator {
    public BetaMessageAccumulator();

    public void accumulate(RawMessageStreamEvent event);
    public BetaMessage message();
    public <T> StructuredMessage<T> message(Class<T> outputClass);
}

Streaming example:

import com.anthropic.helpers.BetaMessageAccumulator;
import com.anthropic.models.beta.messages.StructuredMessageCreateParams;
import com.anthropic.core.http.StreamResponse;

// Create structured output parameters
StructuredMessageCreateParams<BookList> params = MessageCreateParams.builder()
    .model(Model.CLAUDE_SONNET_4_5_20250929)
    .maxTokens(2048)
    .outputFormat(BookList.class)
    .addUserMessage("List famous novels from the 20th century.")
    .build();

// Create streaming request
StreamResponse<RawMessageStreamEvent> stream =
    client.beta().messages().createStreaming(params);

// Accumulate events
BetaMessageAccumulator accumulator = new BetaMessageAccumulator();
stream.stream().forEach(event -> {
    accumulator.accumulate(event);

    // Process events as they arrive
    if (event.isContentBlockDelta()) {
        System.out.print(event.asContentBlockDelta().delta().text());
    }
});

// Get structured message after streaming completes
StructuredMessage<BookList> message = accumulator.message(BookList.class);
BookList books = message.getStructuredContent();

System.out.println("\n\nComplete book list:");
books.books.forEach(book ->
    System.out.println(book.title + " by " + book.author.name)
);

Generic Type Erasure

Java's generic type erasure prevents runtime type information from being available for local variables and parameters. This limits schema derivation to class fields only.

Works (field with generic type):

class BookList {
    public List<Book> books;  // Generic type preserved in field metadata
}

StructuredMessageCreateParams<BookList> params = MessageCreateParams.builder()
    .outputFormat(BookList.class)  // Can derive schema from BookList.books field
    .build();

Does NOT work (local variable with generic type):

List<Book> books = new ArrayList<>();

StructuredMessageCreateParams<List<Book>> params = MessageCreateParams.builder()
    .outputFormat(books.getClass())  // Type erasure: only knows it's a List, not List<Book>
    .build();  // Cannot generate valid schema

Solution: Always wrap collections in a class with typed fields:

class BookCollection {
    public List<Book> items;
}

StructuredMessageCreateParams<BookCollection> params = MessageCreateParams.builder()
    .outputFormat(BookCollection.class)
    .build();

Error Handling

JSON Conversion Errors

When JSON response cannot be converted to the specified Java class, an exception is thrown with the JSON response included for diagnosis.

Common causes:

  • Response truncated (reached max_tokens limit)
  • JSON syntax errors in response
  • Type mismatches between schema and response
  • Missing required fields in response

Example error:

Failed to deserialize JSON response into class BookList
JSON response: {"books":[{"title":"1984","author":{"name":"George Orwell"

Security consideration: Error messages include the JSON response, which may contain sensitive information. Avoid logging errors directly in production environments, or redact sensitive data first.

Schema Validation Errors

Local validation errors occur when the derived schema violates Anthropic's restrictions.

Common causes:

  • Unsupported data types (e.g., LocalDate without custom serializer)
  • Classes with no properties (all fields excluded or no fields defined)
  • Map types used as field types
  • Unsupported constraint values

Example error:

Schema validation failed for class Book:
- Property 'publishDate' has unsupported type 'java.time.LocalDate'
- Use String with @Schema(format="date") instead

Resolution: Either fix the class definition or disable local validation with JsonSchemaLocalValidation.NO if using newer API features.

Best Practices

Field Definition Patterns

Prefer public fields for simple data classes:

class Book {
    public String title;
    public String author;
    public int year;
}

Use private fields with getters for encapsulation:

class Book {
    private String title;
    private String author;

    public String getTitle() { return title; }
    public String getAuthor() { return author; }
}

Use Optional<T> sparingly:

class Book {
    public String title;           // Core data: required
    public String author;          // Core data: required
    public Optional<String> isbn;  // Truly optional metadata
}

Schema Validation

Let local validation catch errors early:

try {
    StructuredMessageCreateParams<Book> params = MessageCreateParams.builder()
        .outputFormat(Book.class)  // Validates schema immediately
        .build();
} catch (AnthropicException e) {
    System.err.println("Invalid schema: " + e.getMessage());
    // Fix class definition before making API calls
}

Disable validation only when necessary:

// Only use when local validation is incorrect
params = MessageCreateParams.builder()
    .outputFormat(Book.class, JsonSchemaLocalValidation.NO)
    .build();

Annotations

Add descriptions for better AI responses:

@JsonClassDescription("A published book with author and publication information")
class Book {
    @JsonPropertyDescription("The full title of the book")
    public String title;

    @JsonPropertyDescription("The primary author of the book")
    public Person author;

    @JsonPropertyDescription("The year the book was first published")
    public int publicationYear;
}

Add constraints for validation:

class Article {
    @Schema(minLength = 1, maxLength = 200)
    public String title;

    @ArraySchema(minItems = 1, maxItems = 10)
    public List<String> authors;

    @Schema(minimum = "1", maximum = "1000")
    public int pageCount;
}

Complete Example

Define data classes:

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import java.util.Optional;

@JsonClassDescription("A person with biographical information")
class Person {
    @JsonPropertyDescription("The full name of the person")
    public String name;

    @Schema(minimum = "1800", maximum = "2100")
    public int birthYear;

    @JsonPropertyDescription("The year the person died, or empty if still living")
    public Optional<Integer> deathYear;
}

@JsonClassDescription("A published book with metadata")
class Book {
    @JsonPropertyDescription("The full title of the book")
    @Schema(minLength = 1, maxLength = 200)
    public String title;

    @JsonPropertyDescription("The primary author of the book")
    public Person author;

    @Schema(minimum = "1800", maximum = "2100")
    public int publicationYear;

    @Schema(pattern = "^(?:ISBN(?:-13)?:?\\ )?(?=[0-9]{13}$|(?=(?:[0-9]+[-\\ ]){4})[-\\ 0-9]{17}$)97[89][-\\ ]?[0-9]{1,5}[-\\ ]?[0-9]+[-\\ ]?[0-9]+[-\\ ]?[0-9]$")
    public Optional<String> isbn;
}

@JsonClassDescription("A collection of books")
class BookList {
    @ArraySchema(minItems = 1, maxItems = 20)
    public List<Book> books;
}

Create and execute request:

import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.beta.messages.StructuredMessage;
import com.anthropic.models.beta.messages.StructuredMessageCreateParams;
import com.anthropic.models.messages.Model;

AnthropicClient client = AnthropicOkHttpClient.fromEnv();

StructuredMessageCreateParams<BookList> params = MessageCreateParams.builder()
    .model(Model.CLAUDE_SONNET_4_5_20250929)
    .maxTokens(4096)
    .outputFormat(BookList.class)
    .addUserMessage("List 5 famous novels from the late 20th century (1950-2000).")
    .build();

try {
    StructuredMessage<BookList> response = client.beta().messages().create(params);
    BookList result = response.getStructuredContent();

    System.out.println("Famous 20th century novels:");
    result.books.forEach(book -> {
        System.out.printf("%s by %s (%d)%n",
            book.title,
            book.author.name,
            book.publicationYear);
        book.isbn.ifPresent(isbn -> System.out.println("  ISBN: " + isbn));
    });
} catch (AnthropicException e) {
    System.err.println("Error: " + e.getMessage());
}

Install with Tessl CLI

npx tessl i tessl/maven-com-anthropic--anthropic-java@2.11.1

docs

client-setup.md

errors.md

index.md

messages.md

platform-adapters.md

streaming.md

structured-outputs.md

tools.md

tile.json