Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{{paramName}}}: {{>optionalDataType}}{{/isQueryParam}}
{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isModel}}{{^queryIsJsonMimeType}} @ModelAttribute("{{baseName}}"){{/queryIsJsonMimeType}}{{#queryIsJsonMimeType}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{/queryIsJsonMimeType}}{{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{{paramName}}}: {{>optionalDataType}}{{/isQueryParam}}
Original file line number Diff line number Diff line change
Expand Up @@ -6431,6 +6431,56 @@ public void suspendFunctionsDefaultsToFalse() throws Exception {
"suspend should not be present when suspendFunctions is not enabled");
}

// ========== MODEL QUERY PARAM @ModelAttribute TESTS ==========

@Test
public void modelQueryParamsGetModelAttributeAnnotation() throws Exception {
// Complex query params (isModel=true) must get @ModelAttribute("baseName") so that:
// - Spring binds nested fields (e.g. filter[statuses]=...) correctly
// - The exact wire name is preserved when paramName is sanitized
// (e.g. 'order-status' → paramName 'orderStatus'; the annotation keeps the wire name)
Map<String, Object> additionalProperties = new HashMap<>();
additionalProperties.put(INTERFACE_ONLY, "true");
additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");
additionalProperties.put(USE_TAGS, "true");

Map<String, File> files = generateFromContract(
"src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties);

File ordersApi = files.get("OrdersApi.kt");
// valid Kotlin identifier — @ModelAttribute still required for binding
assertFileContains(ordersApi.toPath(), "@ModelAttribute(\"filter\")");
// non-Kotlin-identifier names — wire name must be explicit in the annotation
assertFileContains(ordersApi.toPath(), "@ModelAttribute(\"order-status\")");
assertFileContains(ordersApi.toPath(), "@ModelAttribute(\"item-status\")");
// @RequestParam must NOT be used for any model param
assertFileNotContains(ordersApi.toPath(), "@RequestParam(value = \"filter\"");
assertFileNotContains(ordersApi.toPath(), "@RequestParam(value = \"order-status\"");
assertFileNotContains(ordersApi.toPath(), "@RequestParam(value = \"item-status\"");
}

@Test
public void contentJsonQueryParamGetsRequestParamNotModelAttribute() throws Exception {
// A query param declared with `content: application/json` (queryIsJsonMimeType=true) carries a
// single JSON-encoded string on the wire (e.g. ?filter={"ids":[1,2]}).
// @ModelAttribute is for form-field/deepObject binding and would break JSON params even when a
// custom Converter<String,T> is registered — Spring would try to bind filter.ids=... style fields
// rather than deserializing the whole string.
// The correct annotation is @RequestParam, which delivers the raw string to the converter.
Map<String, Object> additionalProperties = new HashMap<>();
additionalProperties.put(INTERFACE_ONLY, "true");
additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true");

Map<String, File> files = generateFromContract(
"src/test/resources/3_0/query-param-json.yaml", additionalProperties);

File searchApi = files.get("SearchApi.kt");
// @RequestParam must be used so the raw JSON string is passed to a Converter<String,Filter>
assertFileContains(searchApi.toPath(), "@RequestParam(value = \"filter\"");
// @ModelAttribute must NOT be used — it would attempt deepObject field binding, not JSON parsing
assertFileNotContains(searchApi.toPath(), "@ModelAttribute(\"filter\")");
}

@Test
public void suspendFunctionsWithServiceInterface() throws Exception {
Path root = generateApiSources(Map.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ servers:
tags:
- name: pet
description: Everything about your Pets
- name: orders
description: Order filtering endpoints
paths:
/pet/findByStatusWithSort:
get:
Expand Down Expand Up @@ -540,6 +542,94 @@ paths:
type: array
items:
$ref: '#/components/schemas/Pet'
# ---- deepObject model query param test cases ----

/orders:
get:
tags:
- orders
summary: List orders filtered by status — tests @ModelAttribute binding for model query params
operationId: listOrders
parameters:
- name: filter
in: query
description: Filter by order status (valid Kotlin identifier)
style: deepObject
explode: true
schema:
$ref: '#/components/schemas/OrderStatus'
- name: order-status
in: query
description: Filter by order status (non-Kotlin-identifier wire name, hyphenated)
style: deepObject
explode: true
schema:
$ref: '#/components/schemas/OrderStatus'
- name: item-status
in: query
description: Filter by item status (non-Kotlin-identifier wire name, hyphenated)
style: deepObject
explode: true
schema:
$ref: '#/components/schemas/OrderStatus'
responses:
'200':
description: OK

/orders/filter-only:
get:
tags:
- orders
summary: List orders with only the filter param — impl asserts filter is bound correctly
operationId: listOrdersFilterOnly
parameters:
- name: filter
in: query
description: Filter by order status (valid Kotlin identifier, isolated test)
style: deepObject
explode: true
schema:
$ref: '#/components/schemas/OrderStatus'
responses:
'200':
description: OK — impl assertion passed

/orders/order-status-only:
get:
tags:
- orders
summary: List orders with only the order-status param — impl asserts order-status is bound correctly
operationId: listOrdersOrderStatusOnly
parameters:
- name: order-status
in: query
description: Filter by order status (non-Kotlin-identifier wire name, isolated test)
style: deepObject
explode: true
schema:
$ref: '#/components/schemas/OrderStatus'
responses:
'200':
description: OK — impl assertion passed

/orders/item-status-only:
get:
tags:
- orders
summary: List orders with only the item-status param — impl asserts item-status is bound correctly
operationId: listOrdersItemStatusOnly
parameters:
- name: item-status
in: query
description: Filter by item status (non-Kotlin-identifier wire name, isolated test)
style: deepObject
explode: true
schema:
$ref: '#/components/schemas/OrderStatus'
responses:
'200':
description: OK — impl assertion passed

# ---- openApiNullable / 4-state required×nullable test cases ----

/nullable/check-required-only:
Expand Down Expand Up @@ -627,6 +717,14 @@ components:
- "id,desc"
- "createdAt,asc"
- "createdAt,desc"
OrderStatus:
type: object
properties:
statuses:
type: array
uniqueItems: true
items:
type: string
Pet:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ settings.gradle
src/main/kotlin/org/openapitools/api/ApiUtil.kt
src/main/kotlin/org/openapitools/api/Exceptions.kt
src/main/kotlin/org/openapitools/api/NullableApi.kt
src/main/kotlin/org/openapitools/api/OrdersApi.kt
src/main/kotlin/org/openapitools/api/PetApi.kt
src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
src/main/kotlin/org/openapitools/configuration/ValidPageable.kt
src/main/kotlin/org/openapitools/configuration/ValidSort.kt
src/main/kotlin/org/openapitools/model/NullableModel.kt
src/main/kotlin/org/openapitools/model/OrderStatus.kt
src/main/kotlin/org/openapitools/model/Pet.kt
src/main/kotlin/org/openapitools/model/PetSort.kt
src/main/kotlin/org/openapitools/model/PetSortEnum.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.23.0-SNAPSHOT).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package org.openapitools.api

import org.openapitools.model.OrderStatus
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity

import org.springframework.web.bind.annotation.*
import org.springframework.validation.annotation.Validated
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.beans.factory.annotation.Autowired

import jakarta.validation.constraints.DecimalMax
import jakarta.validation.constraints.DecimalMin
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size
import jakarta.validation.Valid

import kotlin.collections.List
import kotlin.collections.Map

@RestController
@Validated
@RequestMapping("\${api.base-path:/v2}")
interface OrdersApi {


@RequestMapping(
method = [RequestMethod.GET],
// "/orders"
value = [PATH_LIST_ORDERS]
)
fun listOrders(
@Valid @ModelAttribute("filter") filter: OrderStatus?,
@Valid @ModelAttribute("order-status") orderStatus: OrderStatus?,
@Valid @ModelAttribute("item-status") itemStatus: OrderStatus?
): ResponseEntity<Unit> {
return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
}


@RequestMapping(
method = [RequestMethod.GET],
// "/orders/filter-only"
value = [PATH_LIST_ORDERS_FILTER_ONLY]
)
fun listOrdersFilterOnly(
@Valid @ModelAttribute("filter") filter: OrderStatus?
): ResponseEntity<Unit> {
return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
}


@RequestMapping(
method = [RequestMethod.GET],
// "/orders/item-status-only"
value = [PATH_LIST_ORDERS_ITEM_STATUS_ONLY]
)
fun listOrdersItemStatusOnly(
@Valid @ModelAttribute("item-status") itemStatus: OrderStatus?
): ResponseEntity<Unit> {
return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
}


@RequestMapping(
method = [RequestMethod.GET],
// "/orders/order-status-only"
value = [PATH_LIST_ORDERS_ORDER_STATUS_ONLY]
)
fun listOrdersOrderStatusOnly(
@Valid @ModelAttribute("order-status") orderStatus: OrderStatus?
): ResponseEntity<Unit> {
return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
}

companion object {
//for your own safety never directly reuse these path definitions in tests
const val BASE_PATH: String = "/v2"
const val PATH_LIST_ORDERS: String = "/orders"
const val PATH_LIST_ORDERS_FILTER_ONLY: String = "/orders/filter-only"
const val PATH_LIST_ORDERS_ITEM_STATUS_ONLY: String = "/orders/item-status-only"
const val PATH_LIST_ORDERS_ORDER_STATUS_ONLY: String = "/orders/order-status-only"
}
}
Loading
Loading