OpenAPI·Java Spring·Free

OpenAPI to
Java Spring.

Turn an OpenAPI 3.0 or 3.1 spec into a Spring RestClient-based HTTP client. Java 17 records for models, sealed interfaces for oneOf, raw return types with no forced ResponseEntity, and null-safe optional query params via queryParamIfPresent. No signup, no install.

OpenAPI 3.0 / 3.1·Spring Boot 3·Java 17+·RestClient·MIT
payments.openapi.yamlInput
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
compile
src/main/java/com/example/payments/client/DefaultClient.java tsc cleanOutput
// 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;

@org.springframework.stereotype.Service
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>() {});
    }
}
What gets generated

Records, a sealed union, 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 interface with permits + Jackson @JsonTypeInfo/@JsonSubTypes wiring so the discriminator round-trips both ways.

// src/main/java/com/example/payments/model/Payment.java
package com.example.payments.model;

import jakarta.annotation.Nullable;

public record Payment(String id, Double amount, @Nullable String currency,
                      PaymentStatus status, MethodUnion method) {
}

// src/main/java/com/example/payments/model/MethodUnion.java
package com.example.payments.model;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonSubTypes;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind")
@JsonSubTypes({
    @JsonSubTypes.Type(value = CardPayment.class, name = "card"),
    @JsonSubTypes.Type(value = BankPayment.class, name = "bank")
})
public sealed interface MethodUnion permits CardPayment, BankPayment {
}

// 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 {
}
Three things we get right

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.

01ProofRecords, not POJOs.Spec · InCode · Out
spec.yaml fragment
components:
  schemas:
    Payment:
      type: object
      required: [id, amount, status]
      properties:
        id:     { type: string }
        amount: { type: number, format: double }
        status: { $ref: '#/components/schemas/PaymentStatus' }
emitted .java files
// 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) { /* ... */ }
}
Spring Boot 3 · RestClient

Fluent synchronous HTTP, no reactive overhead.

An abstract BaseRestClient holds the configured RestClient; DefaultClient extends it and exposes one method per operation. The generated client carries @org.springframework.stereotype.Service, so component scanning picks it up and any consumer can inject it by type — no @Bean declaration, no configuration class to maintain.

src/main/java/com/example/payments/client/DefaultClient.java generated
// 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;

@org.springframework.stereotype.Service
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>() {});
    }
}

// application.yml
api:
  base-url: https://api.example.com

// Injected anywhere — component scanning picks up @Service:
@org.springframework.stereotype.Service
public class CheckoutService {
    private final DefaultClient payments;
    public CheckoutService(DefaultClient payments) {
        this.payments = payments;
    }
}
Also available via MCP

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."
See the MCP page
Prefer it in your build?

Same generator, as a package.

The converter above is the fastest way to try the output. For CI and repeatable builds, the identical generator ships as a package — pin a version, wire it into your pipeline, and review diffs like any other code.

Questions

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.

One record per schema, one value-bound enum per enum, a sealed interface per oneOf, and a RestClient-based client class per tag (DefaultClient when no tags are set) — all sharing a single BaseRestClient. Client classes are annotated @Service so component scanning wires them automatically. No controller stubs, no server scaffolding — client only.
More converters

Same engine, other stacks.

Every converter runs the same spec-to-IR pipeline. Pick another source format or target stack.

OpenAPITypeScript
Angular httpResource, React TanStack Query, or framework-agnostic fetch.
Open converter
OpenAPIAngular
Typed services with httpResource, Signals, inject() DI, and interceptors.
Open converter
OpenAPIReact
TanStack Query hooks with stable query keys, or plain async functions.
Open converter
OpenAPIFetch
Framework-agnostic fetch client with ApiResult, middleware, and retries.
Open converter
OpenAPIPython
Async httpx clients paired with Pydantic v2 models.
Open converter
OpenAPIGo
Idiomatic net/http client, context-aware, pointer-nullable fields.
Open converter
OpenAPIKotlin
Ktor client, kotlinx.serialization, sealed interfaces for oneOf.
Open converter
OpenAPIC#
Records, nullable reference types, JsonPolymorphic, HttpClient.
Open converter
OpenAPIRust
reqwest client with serde-tagged enums for oneOf and Option<T>.
Open converter
GraphQLAngular
Observable services with typed queries, mutations, and graphql-ws subscriptions.
Open converter
GraphQLReact
TanStack Query hooks with discriminated-union types and query keys.
Open converter
GraphQLKotlin
Ktor, kotlinx.serialization, Flow-based subscriptions.
Open converter
GraphQLC#
Records, nullable types, [JsonPolymorphic] unions, IAsyncEnumerable subscriptions.
Open converter
ProtobufKotlin
Ktor, coroutines, and sealed classes straight from .proto.
Open converter
ProtobufC#
Records, nullable types, Connect RPC over HttpClient.
Open converter
ProtobufGo
Plain structs with json tags, pointer presence, stdlib-only Connect client.
Open converter
SQLTypeScript
Postgres DDL to typed interfaces with foreign-key navigation.
Open converter
SQLKotlin
CREATE TABLE scripts to Kotlin @Serializable data classes.
Open converter

Try it on your own spec.

The converter runs in the browser. Your spec never leaves the page.

Open the converter