OpenAPI·Java Spring·Free

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·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;

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 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 {
}
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. You wire DefaultClient yourself — as a @Bean in a @Configuration class, or wherever fits your composition root.

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;

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>() {});
    }
}
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
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 marker interface per 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.

Open the converter