The Anthropic Java SDK provides convenient access to the Anthropic REST API from applications written in Java
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.
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:
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 fromvalidation - Whether to validate the schema locally (default: JsonSchemaLocalValidation.YES)Returns: A StructuredMessageCreateParams.Builder<T> for building type-safe parameters
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();
}
}Classes used for structured outputs must follow specific rules to ensure valid JSON schema generation:
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;
}
}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>T[])Each class must define at least one property. A validation error occurs if:
@JsonIgnore@JsonProperty annotationsMap type (maps have no named properties)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");
}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:
Optional<T> fields are not included in required arrayLow-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();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));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
}Local Validation (default):
Remote Validation:
Disable local validation when:
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 Databind annotations control schema generation and add descriptive metadata.
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;
}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;
}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
}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/OpenAPI annotations add type-specific constraints to schema properties.
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 descriptionformat - 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 stringsminLength - Minimum string lengthmaxLength - Maximum string lengthExample:
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;
}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"
}Structured outputs work with streaming by accumulating JSON strings from stream events and deserializing them into Java objects after the stream completes.
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)
);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 schemaSolution: Always wrap collections in a class with typed fields:
class BookCollection {
public List<Book> items;
}
StructuredMessageCreateParams<BookCollection> params = MessageCreateParams.builder()
.outputFormat(BookCollection.class)
.build();When JSON response cannot be converted to the specified Java class, an exception is thrown with the JSON response included for diagnosis.
Common causes:
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.
Local validation errors occur when the derived schema violates Anthropic's restrictions.
Common causes:
LocalDate without custom serializer)Example error:
Schema validation failed for class Book:
- Property 'publishDate' has unsupported type 'java.time.LocalDate'
- Use String with @Schema(format="date") insteadResolution: Either fix the class definition or disable local validation with JsonSchemaLocalValidation.NO if using newer API features.
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
}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();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;
}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