OpenAPI to
Java Spring.
Turn an OpenAPI 3.0 spec into a Spring RestClient-based HTTP client. Java 17 records for models, raw return types with no forced ResponseEntity, and null-safe optional query params via queryParamIfPresent. No signup, no install.
openapi: 3.0.3
info:
title: Payments API
version: 1.2.0
paths:
/payments/{id}:
get:
operationId: getPayment
parameters:
- name: id
in: path
required: true
schema: { type: string }
- name: expand
in: query
required: false
schema: { type: string }
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Payment'
components:
schemas:
Payment:
type: object
required: [id, amount, status, method]
properties:
id: { type: string }
amount: { type: number, format: double }
currency: { type: string, nullable: true }
status: { $ref: '#/components/schemas/PaymentStatus' }
method:
oneOf:
- $ref: '#/components/schemas/CardPayment'
- $ref: '#/components/schemas/BankPayment'
discriminator:
propertyName: kind// src/main/java/com/example/payments/client/DefaultClient.java
package com.example.payments.client;
import org.springframework.web.client.RestClient;
import org.springframework.http.MediaType;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.beans.factory.annotation.Value;
import jakarta.annotation.Nullable;
import com.example.payments.model.Payment;
import java.util.Optional;
public class DefaultClient extends BaseRestClient {
public DefaultClient(
RestClient.Builder restClientBuilder,
@Value("${api.base-url}") String baseUrl
) {
super(restClientBuilder, baseUrl);
}
public Payment getPayment(String id, @Nullable String expand) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/payments/{id}")
.queryParamIfPresent("expand", Optional.ofNullable(expand))
.build(id))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(new ParameterizedTypeReference<Payment>() {});
}
}Records, a union interface, a RestClient.
Below: models emitted from a small payments spec with a oneOf discriminator. Each schema becomes a Java 17 record; the oneOf becomes a sealed-style interface the variants implements.
// src/main/java/com/example/payments/model/Payment.java
package com.example.payments.model;
public record Payment(String id, Double amount, String currency,
PaymentStatus status, MethodUnion method) {
}
// src/main/java/com/example/payments/model/MethodUnion.java
package com.example.payments.model;
public interface MethodUnion {
}
// src/main/java/com/example/payments/model/CardPayment.java
package com.example.payments.model;
public record CardPayment(CardPaymentKind kind, String last4)
implements MethodUnion {
}
// src/main/java/com/example/payments/model/BankPayment.java
package com.example.payments.model;
public record BankPayment(BankPaymentKind kind, String iban)
implements MethodUnion {
}Grounded in how the spec reads.
Each claim here is something you can verify by running the converter on your own spec. Click a card to see the spec fragment that triggered it and the Java that came out.
components:
schemas:
Payment:
type: object
required: [id, amount, status]
properties:
id: { type: string }
amount: { type: number, format: double }
status: { $ref: '#/components/schemas/PaymentStatus' }// model/Payment.java
package com.example.payments.model;
public record Payment(String id, Double amount, PaymentStatus status) {
}
// model/PaymentStatus.java (value enum, Jackson-bound)
public enum PaymentStatus {
PENDING("pending"),
SUCCEEDED("succeeded"),
FAILED("failed");
private final String value;
PaymentStatus(String value) { this.value = value; }
@JsonValue public String getValue() { return value; }
@JsonCreator public static PaymentStatus fromValue(String v) { /* ... */ }
}Fluent synchronous HTTP, no reactive overhead.
An abstract BaseRestClient holds the configured RestClient; DefaultClient extends it and exposes one method per operation. You wire DefaultClient yourself — as a @Bean in a @Configuration class, or wherever fits your composition root.
// src/main/java/com/example/payments/client/BaseRestClient.java
package com.example.payments.client;
import org.springframework.web.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
public abstract class BaseRestClient {
protected final RestClient restClient;
public BaseRestClient(RestClient.Builder restClientBuilder,
@Value("${api.base-url}") String baseUrl) {
this.restClient = restClientBuilder.baseUrl(baseUrl).build();
}
}
// src/main/java/com/example/payments/client/DefaultClient.java
package com.example.payments.client;
import org.springframework.web.client.RestClient;
import org.springframework.http.MediaType;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.beans.factory.annotation.Value;
import jakarta.annotation.Nullable;
import com.example.payments.model.Payment;
import com.example.payments.model.CreatePaymentRequest;
import java.util.Optional;
public class DefaultClient extends BaseRestClient {
public DefaultClient(
RestClient.Builder restClientBuilder,
@Value("${api.base-url}") String baseUrl
) {
super(restClientBuilder, baseUrl);
}
public Payment getPayment(String id, @Nullable String expand) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/payments/{id}")
.queryParamIfPresent("expand", Optional.ofNullable(expand))
.build(id))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(new ParameterizedTypeReference<Payment>() {});
}
public Payment createPayment(CreatePaymentRequest requestBody) {
return restClient.post()
.uri("/payments")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(requestBody)
.retrieve()
.body(new ParameterizedTypeReference<Payment>() {});
}
}Or ask Claude to do it.
The same generator ships as a Model Context Protocol server. It calls the same web API this converter uses, so the output is identical. Point Claude Desktop, Cursor, or any MCP-aware agent at a spec and it produces the Java Spring client as a diff — free, no token, no leaving the chat.
// claude-desktop config · .mcp.json
{
"mcpServers": {
"metaengine": {
"command": "npx",
"args": ["-y", "@metaengine/mcp-server"]
}
}
}
// Then in Claude:
// "Load specs/payments.yaml and generate a Java Spring
// client in package com.example.payments."Things people ask.
The questions that come up most when generating Spring clients from OpenAPI. If yours is not here, run the converter — the answer is usually in the output.
oneOf, and a RestClient-based client class per tag (DefaultClient when no tags are set) — all sharing a single BaseRestClient. No controller stubs, no server scaffolding — client only.Try it on your own spec.
The converter runs in the browser. Your spec never leaves the page.