Skip to content

Commit 959fbe6

Browse files
authored
[Java][Spring] useRecords: records as parameter objects (#25351)
A model bound by name from a query parameter is now a record, unless it holds a component that Spring's data binder cannot fill in a record: a renamed model, array or map, a component wrapped in JsonNullable, a container without an initial value, or a property with no fixed type. Scalar components with another documented name carry BindParam with that name, escaped as a Java string literal. Models converted from one string (header, cookie and path models, array items and map values), the members of a oneOf or anyOf query parameter, and everything nested in a parameter object that stays a class, or in a list or map of one, stay classes.
1 parent 6e4a12b commit 959fbe6

17 files changed

Lines changed: 1681 additions & 155 deletions

File tree

‎docs/generators/spring.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
121121
|useJspecify|Use Jspecify for null checks| |false|
122122
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |true|
123123
|useOptional|Use Optional container for optional parameters| |false|
124-
|useRecords|Generate models as Java records where possible (requires Spring Boot 3 or 4). A model that cannot be a record yet stays a class, and the reason is logged: inheritance, additionalProperties, an anyOf or oneOf composition, a byte[] property, more than 250 properties, the FAIL null policy, a property name a record cannot have, or a model bound from request parameters. Not supported with hateoas, Lombok annotations, withXml and useOptional.| |false|
124+
|useRecords|Generate models as Java records where possible (requires Spring Boot 3 or 4). A model that cannot be a record yet stays a class, and the reason is logged: inheritance, additionalProperties, an anyOf or oneOf composition, a byte[] property, more than 250 properties, the FAIL null policy, a property name a record cannot have, or a request-parameter model that Spring's data binder cannot fill as a record. Not supported with hateoas, Lombok annotations, withXml and useOptional.| |false|
125125
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|
126126
|useSealed|Whether to generate sealed model interfaces and classes| |false|
127127
|useSpringBoot3|Generate code and provide dependencies for use with Spring Boot ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |true|

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java‎

Lines changed: 179 additions & 29 deletions
Large diffs are not rendered by default.

‎modules/openapi-generator/src/main/resources/JavaSpring/record.mustache‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ public record {{classname}}(
7171
{{#swagger2AnnotationLibrary}}
7272
@Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}{{#isNullable}}, nullable = true{{/isNullable}})
7373
{{/swagger2AnnotationLibrary}}
74+
{{#vendorExtensions.x-record-bind-param}}
75+
@org.springframework.web.bind.annotation.BindParam("{{{.}}}")
76+
{{/vendorExtensions.x-record-bind-param}}
7477
{{#jackson}}
7578
{{>jackson_annotations}}
7679
{{/jackson}}

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java‎

Lines changed: 440 additions & 15 deletions
Large diffs are not rendered by default.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Record parameter objects that Spring's data binder cannot fill
4+
version: 1.0.0
5+
paths:
6+
/model:
7+
get:
8+
operationId: model
9+
parameters:
10+
- {name: q, in: query, schema: {$ref: '#/components/schemas/RenamedModel'}}
11+
responses:
12+
'200': {description: ok}
13+
/array:
14+
get:
15+
operationId: array
16+
parameters:
17+
- {name: q, in: query, schema: {$ref: '#/components/schemas/RenamedArray'}}
18+
responses:
19+
'200': {description: ok}
20+
/map:
21+
get:
22+
operationId: map
23+
parameters:
24+
- {name: q, in: query, schema: {$ref: '#/components/schemas/RenamedMap'}}
25+
responses:
26+
'200': {description: ok}
27+
/nullable-model:
28+
get:
29+
operationId: nullableModel
30+
parameters:
31+
- {name: q, in: query, schema: {$ref: '#/components/schemas/NullableModel'}}
32+
responses:
33+
'200': {description: ok}
34+
/nullable-container:
35+
get:
36+
operationId: nullableContainer
37+
parameters:
38+
- {name: q, in: query, schema: {$ref: '#/components/schemas/NullableContainer'}}
39+
responses:
40+
'200': {description: ok}
41+
/plain-container:
42+
get:
43+
operationId: plainContainer
44+
parameters:
45+
- {name: q, in: query, schema: {$ref: '#/components/schemas/PlainContainer'}}
46+
responses:
47+
'200': {description: ok}
48+
/wrapped-scalar:
49+
get:
50+
operationId: wrappedScalar
51+
parameters:
52+
- {name: q, in: query, schema: {$ref: '#/components/schemas/WrappedScalar'}}
53+
responses:
54+
'200': {description: ok}
55+
/any-type:
56+
get:
57+
operationId: anyType
58+
parameters:
59+
- {name: q, in: query, schema: {$ref: '#/components/schemas/AnyType'}}
60+
responses:
61+
'200': {description: ok}
62+
/free-form:
63+
get:
64+
operationId: freeForm
65+
parameters:
66+
- {name: q, in: query, schema: {$ref: '#/components/schemas/FreeForm'}}
67+
responses:
68+
'200': {description: ok}
69+
components:
70+
schemas:
71+
WrappedScalar:
72+
type: object
73+
properties:
74+
name:
75+
type: string
76+
nullable: true
77+
AnyType:
78+
type: object
79+
properties:
80+
anyType: {}
81+
name:
82+
type: string
83+
FreeForm:
84+
type: object
85+
properties:
86+
free:
87+
type: object
88+
name:
89+
type: string
90+
RenamedModel:
91+
type: object
92+
properties:
93+
my-sub:
94+
$ref: '#/components/schemas/BelowRenamedModel'
95+
BelowRenamedModel:
96+
type: object
97+
properties:
98+
value:
99+
type: string
100+
deeper:
101+
$ref: '#/components/schemas/DeeperRenamedModel'
102+
DeeperRenamedModel:
103+
type: object
104+
properties:
105+
value:
106+
type: string
107+
RenamedArray:
108+
type: object
109+
properties:
110+
my-list:
111+
type: array
112+
items:
113+
type: string
114+
below:
115+
$ref: '#/components/schemas/BelowRenamedArray'
116+
BelowRenamedArray:
117+
type: object
118+
properties:
119+
value:
120+
type: string
121+
RenamedMap:
122+
type: object
123+
properties:
124+
my-map:
125+
type: object
126+
additionalProperties:
127+
type: string
128+
below:
129+
$ref: '#/components/schemas/BelowRenamedMap'
130+
BelowRenamedMap:
131+
type: object
132+
properties:
133+
value:
134+
type: string
135+
NullableModel:
136+
type: object
137+
properties:
138+
inner:
139+
nullable: true
140+
allOf:
141+
- $ref: '#/components/schemas/BelowNullableModel'
142+
BelowNullableModel:
143+
type: object
144+
properties:
145+
value:
146+
type: string
147+
NullableContainer:
148+
type: object
149+
properties:
150+
values:
151+
type: array
152+
nullable: true
153+
items:
154+
type: string
155+
below:
156+
$ref: '#/components/schemas/BelowNullableContainer'
157+
BelowNullableContainer:
158+
type: object
159+
properties:
160+
value:
161+
type: string
162+
PlainContainer:
163+
type: object
164+
properties:
165+
values:
166+
type: array
167+
items:
168+
type: string
169+
below:
170+
$ref: '#/components/schemas/BelowPlainContainer'
171+
BelowPlainContainer:
172+
type: object
173+
properties:
174+
value:
175+
type: string
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Record parameter objects given as compositions
4+
version: 1.0.0
5+
paths:
6+
/one-of:
7+
get:
8+
operationId: oneOfQuery
9+
parameters:
10+
- {name: q, in: query, schema: {$ref: '#/components/schemas/PickAlias'}}
11+
responses:
12+
'200': {description: ok}
13+
/any-of:
14+
get:
15+
operationId: anyOfQuery
16+
parameters:
17+
- name: q
18+
in: query
19+
schema:
20+
anyOf:
21+
- $ref: '#/components/schemas/Bird'
22+
- $ref: '#/components/schemas/Fish'
23+
responses:
24+
'200': {description: ok}
25+
/single:
26+
get:
27+
operationId: singleQuery
28+
parameters:
29+
- name: q
30+
in: query
31+
schema:
32+
allOf:
33+
- $ref: '#/components/schemas/Single'
34+
responses:
35+
'200': {description: ok}
36+
/two:
37+
get:
38+
operationId: twoQuery
39+
parameters:
40+
- name: q
41+
in: query
42+
schema:
43+
allOf:
44+
- $ref: '#/components/schemas/PartA'
45+
- $ref: '#/components/schemas/PartB'
46+
responses:
47+
'200': {description: ok}
48+
components:
49+
schemas:
50+
PickAlias:
51+
$ref: '#/components/schemas/Pick'
52+
Pick:
53+
oneOf:
54+
- $ref: '#/components/schemas/Cat2'
55+
- $ref: '#/components/schemas/Dog2'
56+
discriminator:
57+
propertyName: kind
58+
Cat2:
59+
type: object
60+
required: [kind]
61+
properties:
62+
kind: {type: string}
63+
lives: {type: integer}
64+
toy:
65+
$ref: '#/components/schemas/Toy'
66+
Dog2:
67+
type: object
68+
required: [kind]
69+
properties:
70+
kind: {type: string}
71+
bark: {type: string}
72+
Toy:
73+
type: object
74+
properties:
75+
name: {type: string}
76+
Bird:
77+
type: object
78+
properties:
79+
wings: {type: integer}
80+
Fish:
81+
type: object
82+
properties:
83+
fins: {type: integer}
84+
Single:
85+
type: object
86+
properties:
87+
name: {type: string}
88+
part:
89+
$ref: '#/components/schemas/Part'
90+
Part:
91+
type: object
92+
properties:
93+
value: {type: string}
94+
PartA:
95+
type: object
96+
properties:
97+
a: {type: string}
98+
PartB:
99+
type: object
100+
properties:
101+
b: {type: string}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Record parameter objects with a model component that has a default
4+
version: 1.0.0
5+
paths:
6+
/filled:
7+
get:
8+
operationId: filled
9+
parameters:
10+
- {name: q, in: query, schema: {$ref: '#/components/schemas/Holder'}}
11+
responses:
12+
'200': {description: ok}
13+
/empty:
14+
get:
15+
operationId: empty
16+
parameters:
17+
- {name: q, in: query, schema: {$ref: '#/components/schemas/EmptyHolder'}}
18+
responses:
19+
'200': {description: ok}
20+
/plain:
21+
get:
22+
operationId: plain
23+
parameters:
24+
- {name: q, in: query, schema: {$ref: '#/components/schemas/PlainHolder'}}
25+
responses:
26+
'200': {description: ok}
27+
/body:
28+
post:
29+
operationId: body
30+
requestBody:
31+
content:
32+
application/json:
33+
schema:
34+
$ref: '#/components/schemas/BodyHolder'
35+
responses:
36+
'200': {description: ok}
37+
components:
38+
schemas:
39+
Top:
40+
type: object
41+
properties:
42+
k:
43+
type: string
44+
z:
45+
type: string
46+
EmptyTop:
47+
type: object
48+
properties:
49+
k:
50+
type: string
51+
z:
52+
type: string
53+
BodyTop:
54+
type: object
55+
properties:
56+
k:
57+
type: string
58+
z:
59+
type: string
60+
Plain:
61+
type: object
62+
properties:
63+
k:
64+
type: string
65+
Holder:
66+
type: object
67+
properties:
68+
top:
69+
allOf:
70+
- $ref: '#/components/schemas/Top'
71+
default:
72+
k: d
73+
EmptyHolder:
74+
type: object
75+
properties:
76+
top:
77+
allOf:
78+
- $ref: '#/components/schemas/EmptyTop'
79+
default: {}
80+
BodyHolder:
81+
type: object
82+
properties:
83+
top:
84+
allOf:
85+
- $ref: '#/components/schemas/BodyTop'
86+
default:
87+
k: d
88+
PlainHolder:
89+
type: object
90+
properties:
91+
plain:
92+
$ref: '#/components/schemas/Plain'

0 commit comments

Comments
 (0)