Generates client and server side Java/Kotlin code based on OpenAPI spec, using swagger-parser, JavaPoet and KotlinPoet.
- Java or Kotlin?
- Spring or Quarkus?
- Maven, Gradle, or plain CLI?
- Lombok, POJO or Java records for DTOs?
Fill this form and copy the config for your build tool.
<plugin>
<groupId>ru.curs</groupId>
<artifactId>hurdy-gurdy</artifactId>
<version>3.3</version>
<configuration>
<!--Root package for generated code-->
<rootPackage>com.example.project</rootPackage>
<spec>${basedir}/src/main/openapi/api.yaml</spec>
<!--Optional: where generated sources are written
(default target/generated-sources/openapi)-->
<outputDirectory>${project.build.directory}/generated-sources/openapi</outputDirectory>
<!--Set to true if you want to have HttpServletResponse response
parameter in Controller interface: good for server-side code.
Set to false (default) if you don't need one
(good for client-side)-->
<generateResponseParameter>true</generateResponseParameter>
<!--Optional: target web framework (spring|quarkus, default spring)
and which interfaces to generate (any subset of controller,api,client;
default controller) — see "Generated interfaces" below-->
<framework>spring</framework>
<generate>controller,client</generate>
<!--Optional: Java DTO style (lombok|pojo|records, default lombok);
Java only — see "Java DTO styles" below-->
<javaDtoStyle>lombok</javaDtoStyle>
</configuration>
<executions>
<execution>
<goals>
<goal>gen-server</goal>
</goals>
</execution>
</executions>
</plugin>The gen-server goal skips regeneration when nothing the generated sources
depend on has changed since the last run: the spec file and every file it
(transitively) references via $ref: "<file>#/...", the effective plugin
configuration, and the plugin version itself. This is tracked by marker files under target/hurdy-gurdy/.
For several specs in one build, declare one <execution> per spec, each with
its own <configuration> (including a distinct <outputDirectory>):
<executions>
<execution>
<id>gen-petstore</id>
<goals><goal>gen-server</goal></goals>
<configuration>
<spec>${basedir}/src/main/openapi/petstore.yaml</spec>
<rootPackage>com.example.petstore</rootPackage>
</configuration>
</execution>
<execution>
<id>gen-billing</id>
<goals><goal>gen-server</goal></goals>
<configuration>
<spec>${basedir}/src/main/openapi/billing.yaml</spec>
<rootPackage>com.example.billing</rootPackage>
<outputDirectory>${project.build.directory}/generated-sources/billing</outputDirectory>
</configuration>
</execution>
</executions>import ru.curs.hurdygurdy.Framework
import ru.curs.hurdygurdy.Role
import ru.curs.hurdygurdy.Language
plugins {
java
id("ru.curs.hurdy-gurdy") version "3.3"
}
hurdyGurdy {
"petstore" {
spec = layout.projectDirectory.file("src/main/openapi/api.yaml")
rootPackage = "com.example.project"
framework = Framework.SPRING // default SPRING
language = Language.JAVA // default JAVA
generate = setOf(Role.CONTROLLER) // default [CONTROLLER]
generateResponseParameter = true // default false
forceSnakeCaseForProperties = true // default true
}
}Each named block registers a generate<Name> task (e.g. generatePetstore) whose
output dir is added to the main source set, so compileJava/compileKotlin
depend on it automatically. The task is cacheable: unchanged inputs — the spec,
every file it (transitively) references via $ref: "<file>#/...", and the
configuration — keep both generation and dependent compilation UP-TO-DATE.
For Kotlin output, set language = Language.KOTLIN and apply the Kotlin JVM plugin.
import ru.curs.hurdygurdy.Framework
import ru.curs.hurdygurdy.GeneratorParams
import ru.curs.hurdygurdy.KotlinCodegen
import ru.curs.hurdygurdy.Role
val codegen = KotlinCodegen(
GeneratorParams.rootPackage("com.example.project")
// optional: spring is the default
.framework(Framework.QUARKUS)
// optional: controller alone is the default;
// a Kotlin collection works too: .generate(listOf(Role.CONTROLLER, Role.CLIENT))
.generate(Role.CONTROLLER, Role.CLIENT)
)
val yamlPath = project.layout.projectDirectory.asFile.toPath().resolve("src/main/openapi/api.yaml")
val resultPath = project.layout.buildDirectory.get().asFile.toPath().resolve("generated-sources")
Files.createDirectories(resultPath)
codegen.generate(yamlPath, resultPath)Build the executable fat jar:
mvn -DskipTests package
java -jar target/hurdy-gurdy-<version>-cli.jar \
--spec src/main/openapi/api.yaml \
--root-package com.example.project \
--output generated-sources \
--framework spring --generate controller,client
Options: --language java|kotlin, --framework spring|quarkus,
--generate (subset of controller,api,client), --response-parameter,
--force-snake-case (negate with --no-...). Run with --help for the full list.
Optionally build a native binary (requires GraalVM):
mvn -Pnative -DskipTests package
./target/hurdy-gurdy --spec ... --root-package ... --output ...
| Parameter name | Type | Default value | Description |
|---|---|---|---|
rootPackage |
String | Sets the root package for all the generated classes. Controller, Api and Client interfaces will be generated in controller subpackage, and all the DTOs will be generated in dto subpackage. |
|
generateResponseParameter |
boolean | false | Set to true if you need access to the raw HTTP response. For controller: adds an HttpServletResponse parameter to each method (Spring) or makes methods return jakarta.ws.rs.core.Response (Quarkus) — useful for returning specific HTTP status codes; together with the operation-level extension x-include-request: true, the method also receives the request. For client: methods return the HTTP envelope (ResponseEntity<T> for Spring, Response for Quarkus). Never affects api interfaces. |
generateApiInterface |
boolean | false | Deprecated — equivalent to adding api to generate (see below). Kept for backwards compatibility. |
forceSnakeCaseForProperties |
boolean | true | By default, hurdy-gurdy expects all the properties of DTO classes to be defined in snake_case in the specification. It converts these names to camelCase for generated classes and sets Jackson's SnakeCaseStrategy so that they will still be snake_case in JSON representation. If you don't want this (e. g. if you want your properties to be defined in camelCase everywhere) you can turn off this function via this parameter. Leading underscores are a legal part of a snake_case name (_anchors, __meta) and are kept as they are in the generated property name; since Jackson's SnakeCaseStrategy would drop the first one, such properties additionally get an explicit @JsonProperty pinning the name from the specification. |
generateAliasAsModel |
boolean | false | Controls how an alias — a named component schema that is a plain type: array (e.g. ItemArray: {type: array, items: {$ref: Item}}) — is generated; mirrors openapi-generator's parameter of the same name. When false (default), every same-file $ref to the alias is inlined as List<Item> and no ItemArray class is generated. When true, the alias becomes a model of its own — class ItemArray extends ArrayList<Item> (Java) / class ItemArray : ArrayList<Item>() (Kotlin) — and references keep the ItemArray type. Both forms serialize as a plain JSON array. CLI flag: --alias-as-model. Note that x-extends on an array alias is only honored when this is true: in the default (inline) mode there is no alias class to attach the interface to, so an x-extends on the alias is silently ignored (the inlined List<Item> cannot implement it). |
framework |
String (spring|quarkus) |
spring |
Selects the web framework whose annotations are emitted on the generated interfaces. spring (default) emits Spring MVC annotations (@GetMapping, @PathVariable, …). quarkus emits Jakarta REST / Quarkus annotations (@GET + @Path, @PathParam, @QueryParam, @HeaderParam, @RestForm for multipart). Value is case-insensitive. |
generate |
comma-separated subset of controller, api, client |
controller |
Selects which interfaces to generate — any combination in a single run, e.g. <generate>controller,client</generate>. See Generated interfaces. Case-insensitive. |
javaDtoStyle |
String (lombok|pojo|records) |
lombok |
Shape of the generated Java DTOs. lombok (default) emits Lombok @Data classes (requires Lombok on the classpath). pojo emits plain classes with explicit getters/setters plus equals/hashCode/toString — no Lombok dependency. records emits Java records. Applies to Java only; Kotlin always generates data classes. See Java DTO styles. Case-insensitive. |
The generate parameter selects which interfaces are emitted for the API paths — any
subset of controller, api and client, in a single run, all in the controller
subpackage and sharing the same DTOs. Combined with framework, this gives six
possible artifacts:
generate value |
framework=spring |
framework=quarkus |
|---|---|---|
controller |
XxxController — server interface to implement: @GetMapping, …; optional HttpServletResponse parameter |
XxxController — Jakarta REST resource interface to implement: @GET + @Path, …; optional Response return type |
api |
XxxApi — pure contract: same Spring MVC annotations, no response-related artifacts. Directly consumable by Spring Cloud OpenFeign (its default SpringMvcContract parses @GetMapping/@RequestMapping on OpenFeign client interfaces), or as a typed contract for hand-written implementations, e.g. over REST Assured in tests |
XxxApi — pure contract: same Jakarta REST annotations, no response-related artifacts. Directly consumable by MicroProfile REST Client's RestClientBuilder.newBuilder().baseUri(…).build(XxxApi.class) (no @RegisterRestClient needed for the programmatic API), or as a typed contract for hand-written implementations |
client |
XxxClient — Spring 6 HTTP Interface: @GetExchange, …; create a proxy with HttpServiceProxyFactory |
XxxClient — MicroProfile / Quarkus REST Client: @RegisterRestClient interface, inject it with @RestClient |
For example, a Quarkus service that also calls itself from tests (or a sibling service consuming the same spec) can generate both sides at once:
<framework>quarkus</framework>
<generate>controller,client</generate>generateResponseParameter applies per interface kind: it affects controller
(response parameter / Response return) and client (methods return the HTTP
envelope — ResponseEntity<T> for Spring, Response for Quarkus — so callers can
inspect status and headers), and never affects api. Server-only constructs
(HttpServletResponse, @Context ContainerRequestContext, x-include-request)
are omitted from api and client interfaces.
In code, use GeneratorParams.rootPackage(...).generate(Role.CONTROLLER, Role.CLIENT).
For Java output, javaDtoStyle selects the shape of the generated DTO classes.
It applies to Java only — Kotlin always generates data classes. The choice
never changes the JSON wire format: all three styles serialize and deserialize
the same JSON for the same specification.
| Style | What is generated | Notes |
|---|---|---|
lombok (default) |
Lombok @Data classes |
Requires Lombok on the classpath. |
pojo |
Plain classes with explicit getters/setters plus equals/hashCode/toString |
No Lombok dependency. Value semantics match @Data (own fields only). |
records |
Java records | Immutable; requires Java 17+. Record-style name() accessors (not getName()). |
In code:
import ru.curs.hurdygurdy.JavaDtoStyle
val codegen = JavaCodegen(
GeneratorParams.rootPackage("com.example.project")
.javaDtoStyle(JavaDtoStyle.RECORDS)
)lombok and pojo use ordinary Java classes and behave identically in shape:
allOfinheritance → the subtypeextendsthe base class.discriminator→ the base carries@JsonTypeInfo(use = NAME)+@JsonSubTypes; subtypesextendit. When the schema declares no explicitdiscriminator.mapping, the@JsonSubTypesnames are derived from the subtype schema names (the OpenAPI implicit convention), so deserialization works without a hand-written mapping.oneOfand a top-levelanyOfof two or more$refs → an interface carrying@JsonTypeInfo(use = DEDUCTION)+@JsonSubTypes; the member classesimplementit.
records cannot use class inheritance (a Java record is final and cannot
extend), so is-a relationships are expressed through interfaces:
discriminator,oneOf, top-levelanyOfbases becomesealed interfaces thatpermittheir subtypes; each concrete subtype is arecordthatimplementsthe base (and any interface it participates in — a type canimplementseveral).allOf-inherited properties are flattened into the subtype record's components (records inherit no fields). A plainallOfbase (nodiscriminator/oneOf) stays its own record and is still instantiable; the subtype simply repeats its components — the JSON is identical.- Required components are validated in a compact constructor
(
Objects.requireNonNull), so a missing required value fails fast. additionalPropertiesbecome a trailingMapcomponent annotated@JsonAnySetter/@JsonAnyGetter.- A
nullableself-reference and self-referential (recursive) schemas are supported (a record may reference its own type as a component).
Set framework to quarkus (Maven <framework>quarkus</framework>, or
GeneratorParams.rootPackage(...).framework(Framework.QUARKUS) in code) to
generate Jakarta REST interfaces instead of Spring MVC ones. DTO classes are
identical in both modes.
Annotation mapping:
| Concern | Spring | Quarkus (JAX-RS) |
|---|---|---|
| Interface | (none) | @Path("") |
| HTTP method | @GetMapping(value, produces, consumes) |
@GET + @Path(path) + @Produces + @Consumes |
| Path parameter | @PathVariable |
@PathParam |
| Query parameter | @RequestParam |
@QueryParam (+ @DefaultValue) |
| Header parameter | @RequestHeader |
@HeaderParam |
| Request body | @RequestBody |
(unannotated parameter) |
| Multipart part | @RequestPart |
@RestForm |
@Produces is emitted only when the operation defines a success (2xx) response
media type, and @Consumes only when the request body defines a media type.
generateResponseParameter has no servlet analog in Quarkus. When enabled, the
generated Controller method returns jakarta.ws.rs.core.Response (instead of the
DTO), and a Javadoc/KDoc @return line documents the entity type the Response is
expected to carry. When generateResponseParameter is true and the
x-include-request: true operation extension is present, the generated Controller
method also gains a @Context jakarta.ws.rs.container.ContainerRequestContext requestContext parameter (the Quarkus analog of the Spring HttpServletRequest
behavior).
The generated Quarkus code requires jakarta.ws.rs-api on the consuming project's
classpath. For multipart endpoints, it also requires org.jboss.resteasy.reactive.RestForm
and org.jboss.resteasy.reactive.multipart.FileUpload — both provided by the
Quarkus REST extension.
A property is nullable (Kotlin T?; in Java records style, exempt from the
Objects.requireNonNull check) when it is not required, or when the schema
says it may be null. How a schema says that depends on the version of the
document:
| Document | Nullable spelling |
|---|---|
openapi: 3.0.x |
type: string + nullable: true |
openapi: 3.1.x |
type: [string, "null"], or anyOf: [{type: string}, {type: 'null'}] |
nullable is ignored in a 3.1 document. OpenAPI 3.1 adopted JSON Schema
2020-12, which has no nullable keyword — it was
removed outright rather than deprecated,
because a type array expresses the same thing. hurdy-gurdy follows the spec, as
other 3.1 tooling does, so a nullable left over from a 3.0 document means
nothing once the version string says 3.1: a required property carrying it
generates as non-null.
Since that is easy to miss when migrating, every leftover nullable in a 3.1
document is reported as a build warning naming its location, for example:
hurdy-gurdy: 'nullable' is not an OpenAPI 3.1 keyword and is ignored at
#/components/schemas/Thing/properties/req_nullable; use type: [<type>, "null"] instead
The same rule decides the Kotlin signature of an operation: a value is nullable when it may be absent, or when its schema says it may be null.
| Position | Non-null when |
|---|---|
| path parameter | always — it is part of the URL |
| query / header parameter | required: true, or the schema carries a default (the framework substitutes it) |
| request body | the operation says required: true |
| multipart part | the body is required: true and the multipart schema lists the part in its required |
| return type | always — a documented response body is what the endpoint sends back |
put:
operationId: updateItem
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/ItemRequest'}}}
parameters:
- {name: id, in: path, required: true, schema: {type: integer, format: int64}}
- {name: code, in: query, schema: {type: string}}
- {name: force, in: query, schema: {type: boolean, default: false}}
responses:
"200":
description: OK
content: {application/json: {schema: {$ref: '#/components/schemas/ItemResponse'}}}public fun updateItem(
@RequestBody request: ItemRequest,
@PathVariable(name = "id") id: Long,
@RequestParam(required = false, name = "code") code: String?,
@RequestParam(required = false, name = "force", defaultValue = "false") force: Boolean,
): ItemResponseA schema that admits null keeps its ? even where the value is always present,
so a required parameter whose schema is nullable: true (3.0) or
type: [string, "null"] (3.1) still generates as String?.
A request body that is not required also gets @RequestBody(required = false)
(and an optional multipart part @RequestPart(..., required = false)): Spring
defaults both to required = true and would reject the missing body before the
nullable parameter could ever be null.
Changed in 4.0. Kotlin previously marked every parameter, request body and return type nullable (#617). See Upgrading from 3.x to 4.x. Java output is unaffected.
4.0 makes the Kotlin generator read parts of the specification it used to ignore. Nothing in the Java output changes, and nothing changes until you bump the version.
The specification is the source of truth, so every change below has two answers: accept the more precise types and adjust your Kotlin, or state what you actually meant in the spec and keep the types you had. Both are listed.
These are source-breaking, not binary-breaking: Kotlin nullability is metadata, not part of the JVM descriptor, so already-compiled code keeps linking. You will see it when you recompile against regenerated sources.
See Parameters, request bodies and return types for the full rule (#617).
Expect this to touch nearly every method. On two real-world 3.0 specs in the test corpus, 100% of generated methods changed signature — a path parameter or a response body is enough, and most operations have one.
// 3.x // 4.0
fun getItem(id: Long?): Item? fun getItem(id: Long): Item
fun createItem(@RequestBody r: ItemRequest?): Item? fun createItem(@RequestBody r: ItemRequest): ItemAdjust your code: delete the ?. An override whose nullability differs
matches nothing, so the compiler names every site. Callers of a generated
client interface mostly get warnings ("unnecessary safe call") rather than
errors.
Or adjust your spec, if a position really is nullable:
To keep T? |
Write |
|---|---|
| path / query / header parameter | nullable: true on the parameter's schema — works even with required: true |
| request body | required: false, or omit required (OpenAPI's default) |
| response with an inline schema | nullable: true on that schema |
response with a $ref |
nullable: true on the component — affects every use of it |
response with a $ref, at this site only |
not expressible in 3.0 — see below |
An array whose items is a $ref to a component declaring nullable: true
now generates List<Tag?> rather than List<Tag>
(#620). That is
what the document says: in OpenAPI 3.0 nullable: true on a schema means null
is a valid value everywhere that schema is referenced, array elements
included.
Adjust your spec — usually by deleting a flag you did not need. A component
carrying nullable: true so that its optional properties come out nullable is
carrying it for nothing: an optional property is nullable either way.
SensitivityTag:
type: string
nullable: true # <- delete this, and List<SensitivityTag?> becomes List<SensitivityTag>
enum: [LOW, HIGH]// with the flag removed, optional properties are STILL nullable:
public data class Holder(
public val tags: List<SensitivityTag>? = null, // elements no longer nullable
public val oneTag: SensitivityTag? = null, // optional, so nullable regardless
public val tagsRequired: List<SensitivityTag>, // required, so non-null
)nullable: true on a component is only genuinely needed when a required
property of it must accept null — and then nullable elements are correct.
Adjust your code instead if you do mean it: the elements really can be null,
so List<Tag?> is the honest type.
There is no per-site opt-out for a $ref in a 3.0 document. Keywords written
beside a $ref are ignored by the format itself, so this does nothing at all:
items:
$ref: '#/components/schemas/SensitivityTag'
nullable: false # silently ignored - 3.0 ignores siblings of $refIf you need a component nullable in one place and not another, you have three options:
- split the component into two (
TagandNullableTag) — works in 3.0; - wrap it in
allOfat the site, which does carry the keyword, at the cost of an extra generated subclass:schema: {title: MaybeTag, nullable: true, allOf: [{$ref: '#/components/schemas/Tag'}]}
- move to 3.1, which states it at the site:
items: {anyOf: [{$ref: '#/components/schemas/Tag'}, {type: 'null'}]}.
A 3.1 document generates the same code as the 3.0 document it was migrated from: bumping the version string alone must not change a single byte of output, and a test asserts exactly that over the specification corpus.
OpenAPI 3.1 adopted JSON Schema 2020-12, which brings keywords 3.0 could not express. They map as follows:
| Keyword | Generated as | Note |
|---|---|---|
type: [string, "null"] |
String? / nullable |
see the section above |
const: <value> |
the value's own type | const: circle is a String |
contentEncoding: base64 (or base64url) |
byte[] / ByteArray |
the string carries encoded bytes |
contentMediaType alone |
String |
it describes the decoded content; without contentEncoding the JSON value is an ordinary string |
type: array with no items |
List<Object> / List<Any> |
3.1 does not require items |
prefixItems: [...] |
List<Object> / List<Any> |
no tuple type in Java or Kotlin; reported as a warning. Applies even when items is also present, since items then constrains only the elements after the prefix |
type: [string, integer] |
Object / Any |
a multi-type union has no single target type |
true / false as a schema |
Object / Any |
true admits any value |
enum containing null |
enum without the null, schema marked nullable |
the null is nullability, not a constant — and the nullability is kept |
additionalProperties: true or {} |
Map<String, Object> / Map<String, Any?> |
the same free-form dictionary, either spelling, either version |
additionalProperties: false |
no dictionary field | the schema forbids additional properties, either version |
Behaviour change.
additionalProperties: truein a 3.0 document used to generateMap<String, String>. That map cannot hold what the schema permits: Jackson throwsMismatchedInputExceptionon a nested object or array, and silently retypes a number or boolean (42comes back out as"42"). It now generatesMap<String, Object>/Map<String, Any?>, matching whatadditionalProperties: {}already generated and what 3.1 already generated. Consuming code that read those values asStringneeds updating. A dictionary with a declared value type —additionalProperties: {type: string}— is unaffected and still generatesMap<String, String>.
Three things are not supported:
$refinto$defs. hurdy-gurdy generates one class per entry incomponents/schemas, so a pointer that walks further in (e.g.#/components/schemas/Holder/$defs/Local) has no class to name. Move the schema intocomponents/schemasand reference it from there.webhooks. The 3.1 top-levelwebhooksmap is not generated.- 3.1-style binary bodies and file parts. A binary request/response body or
multipart file part is still recognised by
format: binary, the 3.0 spelling. The 3.1 idioms — anapplication/octet-streammedia type with no schema, or a part carrying onlycontentMediaType— are not yet mapped toResource/InputStream/MultipartFile.
With client in the generate set, the emitted XxxClient interfaces are meant to be
called, not implemented — the framework supplies the implementation.
- Spring (
framework=spring): Spring 6 HTTP Interface. Methods carry@GetExchange/@PostExchange/@PutExchange/@PatchExchange/@DeleteExchange; parameters keep the same@PathVariable/@RequestParam/@RequestHeader/@RequestBody/@RequestPartannotations. Create a proxy withHttpServiceProxyFactory. - Quarkus (
framework=quarkus): the interface is additionally annotated@RegisterRestClient; inject it with@RestClient. No implementation is written. See the Quarkus REST Client guide.
generateResponseParameter=true makes client methods return the HTTP envelope so callers can inspect
status/headers: ResponseEntity<T> (Spring) / jakarta.ws.rs.core.Response (Quarkus). With
generateResponseParameter=false they return the deserialized DTO. Server-only constructs
(HttpServletResponse, @Context ContainerRequestContext, x-include-request) are omitted from client
interfaces.
components:
schemas:
#---------------------------------------------------------------------------
# Abstract class with discriminator 'vehicle_type'
#---------------------------------------------------------------------------
'Vehicle':
type: object
nullable: false
properties:
'vehicle_type':
type: string
discriminator:
propertyName: vehicle_type
mapping:
'CAR': '#/components/schemas/Car'
'TRUCK': '#/components/schemas/Truck'
#---------------------------------------------------------------------------
# Concrete classes
#---------------------------------------------------------------------------
'Car':
nullable: false
allOf:
- $ref: "#/components/schemas/Vehicle"
- type: object
properties:
'car_property':
type: string
'Truck':
nullable: false
allOf:
- $ref: "#/components/schemas/Vehicle"
- type: object
properties:
'truck_property':
type: stringThis will produce the following in Java:
//Vehicle.java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "vehicle_type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = Car.class, name = "CAR"),
@JsonSubTypes.Type(value = Truck.class, name = "TRUCK")})
public class Vehicle {
}
//Car.java
@Data
@EqualsAndHashCode(callSuper = true)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Car extends Vehicle {
private String carProperty;
}The @EqualsAndHashCode(callSuper = true) is emitted on subtypes only (a class that
extends a generated parent), so inherited fields participate in equals/hashCode;
base and standalone classes keep a plain @Data.
With javaDtoStyle=pojo the same schema produces plain classes (no Lombok) with
explicit accessors and value methods:
//Car.java
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Car extends Vehicle {
private String carProperty;
public String getCarProperty() {
return this.carProperty;
}
public void setCarProperty(String carProperty) {
this.carProperty = carProperty;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Car that = (Car) o;
return Objects.equals(carProperty, that.carProperty);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), carProperty);
}
@Override
public String toString() {
return "Car{" + "carProperty=" + carProperty + "}";
}
}With javaDtoStyle=records the discriminator base becomes a sealed interface
and each subtype a record, with inherited properties flattened into the
components and required ones null-checked in a compact constructor:
//Vehicle.java
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "vehicle_type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = Car.class, name = "CAR"),
@JsonSubTypes.Type(value = Truck.class, name = "TRUCK")})
public sealed interface Vehicle permits Car, Truck {
}
//Car.java
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Car(String carProperty) implements Vehicle {
}This will produce the following in Kotlin:
//Vehicle.kt
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy::class)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "vehicle_type"
)
@JsonSubTypes(JsonSubTypes.Type(value = Car::class, name = "CAR"),
JsonSubTypes.Type(value = Truck::class, name = "TRUCK"))
public sealed class Vehicle()
//Car.kt
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy::class)
public data class Car(
public val carProperty: String? = null
) : Vehicle()You can use x-extends extended property on schema element in order to make DTO implement given interface or interfaces:
components:
schemas:
MenuItemDTO:
type: object
nullable: false
x-extends:
- java.lang.Serializable
title: MenuItemDTO
properties:
[....]You can use references to external specification files if they are available on the same file system as the original one. However, hurdy-gurdy does not attempt to generate code for referenced specifications: we believe this should be done explicitly for every spec. Hurdy-gurdy just uses x-package extension property on the referenced specification in order to define the location of referenced DTOs.
For example, given the following spec fragment:
/api/v1/external:
get:
operationId: external
responses:
"200":
description: external file
content:
text/csv:
schema:
$ref: 'externalfile.yaml#/components/schemas/DatabaseConnectionRequest'The externalfile.yaml file should be located in the same folder and it should contain x-package property:
openapi: 3.0.1
info:
paths:
x-package: com.exampleThen code generator will suggest that com.example.dto.DatabaseConnectionRequest class exists on the classpath.
