Skip to content
Merged
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
Expand Up @@ -395,17 +395,15 @@ object SpecParser {
val topRequired = schema.required.orEmpty().toSet()
val contextCreator: (String) -> String? = { propName -> "$parentName.${propName.toPascalCase()}" }

val (required, properties) = schema.allOf
.orEmpty()
.fold(topRequired to emptyMap<String, PropertyModel>()) { (accRequired, accProperties), subSchema ->
val resolvedSchema = subSchema.resolveSubSchema()
val mergedRequired = accRequired + resolvedSchema.required.orEmpty().toSet()
mergedRequired to accProperties + resolvedSchema.propertyModels(mergedRequired, contextCreator)
}
val subSchemas = schema.allOf.orEmpty().map { it.resolveSubSchema() }

val required = topRequired + subSchemas.flatMap { it.required.orEmpty() }.toSet()

val properties = subSchemas.fold(emptyMap<String, PropertyModel>()) { accProperties, resolvedSchema ->
accProperties + resolvedSchema.propertyModels(required, contextCreator)
}

val topLevelProperties = schema.propertyModels(required, contextCreator)
val finalProperties =
properties.plus(topLevelProperties).values.map { prop -> prop.copy(nullable = prop.name !in required) }
val finalProperties = properties.plus(schema.propertyModels(required, contextCreator)).values.toList()

return finalProperties to required
}
Expand Down Expand Up @@ -588,7 +586,8 @@ object SpecParser {
name = propName,
type = type,
description = propSchema.description,
nullable = propName !in required && !type.honorsDefault(propSchema.default),
nullable = propSchema.nullable == true ||
(propName !in required && !type.honorsDefault(propSchema.default)),
defaultValue = normalizeDefault(propSchema.default),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,81 @@ class SpecParserTest : SpecParserTestBase() {
assertEquals("NewPet", bodyType.schemaName)
}

@Test
fun `schema-level nullable controls property nullability independent of required`() {
val spec = parseSpec(
"""
openapi: 3.0.0
info:
title: Nullable API
version: 1.0.0
paths: {}
components:
schemas:
Thing:
type: object
required:
- requiredNullable
- requiredPlain
properties:
requiredNullable:
type: string
nullable: true
requiredPlain:
type: string
optionalNullable:
type: string
nullable: true
optionalPlain:
type: string
""".trimIndent().toTempFile(),
)
val thing = spec.schemas.find { it.name == "Thing" } ?: fail("Thing not found")
val props = thing.properties.associateBy { it.name }

assertTrue(props.getValue("requiredNullable").nullable, "required + nullable:true should be nullable")
assertFalse(props.getValue("requiredPlain").nullable, "required without nullable should be non-nullable")
assertTrue(props.getValue("optionalNullable").nullable, "optional + nullable:true should be nullable")
assertTrue(props.getValue("optionalPlain").nullable, "optional should be nullable")
}

@Test
fun `allOf property required in a later member is not nullable`() {
// `foo` is declared (optional) in the first allOf member and marked required in the
// second. After merging it is required, so it must be non-nullable — regardless of the
// order the members are listed. Regression test: nullability must be derived from the
// full merged `required` set, not one accumulated mid-fold.
val spec = parseSpec(
"""
openapi: 3.0.0
info:
title: AllOf API
version: 1.0.0
paths: {}
components:
schemas:
Base:
type: object
properties:
foo:
type: string
RequiresFoo:
type: object
required:
- foo
Combined:
allOf:
- ${'$'}ref: '#/components/schemas/Base'
- ${'$'}ref: '#/components/schemas/RequiresFoo'
""".trimIndent().toTempFile(),
)
val combined = spec.schemas.find { it.name == "Combined" } ?: fail("Combined not found")
val foo = combined.properties.first { it.name == "foo" }

assertTrue("foo" in combined.requiredProperties, "sanity: foo should be required after merge")
assertFalse(foo.nullable, "foo is required (via a later allOf member) and must be non-nullable")
}

@Test
fun `parsed endpoints have tags`() {
val listPets = petstore.endpoints.find { it.operationId == "listPets" }!!
Expand Down
Loading