feat(core): honor schema-level nullable:true in OpenAPI 3.0#83
Conversation
Coverage Report
|
Property nullability was derived solely from the parent's required set, so a required property marked nullable:true was generated as a non-nullable Kotlin type. The parser now treats a property as nullable when it is absent from required OR the schema sets nullable:true. The allOf merge preserves schema-level nullability when recomputing against the union of required sets. Closes #41 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10c3893 to
a4396eb
Compare
…able # Conflicts: # core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt
mfabisiak
left a comment
There was a problem hiding this comment.
Bartek's PR introduced a regression in the allOf merge: a required property could come out nullable (String? instead of String), depending on the order the allOf members are listed. Not reachable on master.
When it manifested — a property defined in one member and marked required in a later member:
Base: # defines `foo` (no required, no nullable)
properties: { foo: { type: string } }
RequiresFoo: # only marks foo required
required: [foo]
Combined:
allOf: [ {$ref: Base}, {$ref: RequiresFoo} ]Combined.foo is required and not marked nullable, so it must be foo: String — but it generated foo: String?. Swap the two members and it's correct: the result depended purely on member order.
Why — the merge folds over members, computing each property's nullability against the required set known so far. foo is built while processing Base (required still empty → nullable = true); the later RequiresFoo adds it to required but never re-computes it, so the stale true sticks. On master the final line was an authoritative reset (nullable = name !in required) that discarded this. The PR changed it to name !in required || prop.nullable to keep the nullable: true marker — but prop.nullable isn't just the marker, it's the stale not-in-partial-required || marker, so OR-ing it back in resurfaced the bug.
Fix — compute the full required set first, then derive properties from it in one pass; the separate recompute is then redundant and removed:
val subSchemas = schema.allOf.orEmpty().map { it.resolveSubSchema() }
val required = topRequired + subSchemas.flatMap { it.required.orEmpty() }.toSet()
val properties = subSchemas.fold(emptyMap<String, PropertyModel>()) { acc, s ->
acc + s.propertyModels(required, contextCreator)
}
val finalProperties = properties.plus(schema.propertyModels(required, contextCreator)).values.toList()Now allOf computes nullability like the non-allOf and inline paths — one pass against the complete required set, no order dependency. nullable: true still works. Added a regression test (allOf property required in a later member is not nullable).
What
OpenAPI 3.0
nullable: truewas ignored — nullability came only from the parent'srequiredset. A property that isrequiredandnullable: truewas generated as a non-nullable Kotlin type, which is incorrect.SpecParser.propertyModelsnow setsnullable = propName !in required || propSchema.nullable == true.allOfmerge preserves schema-level nullability when recomputing against the union of required sets (prop.name !in required || prop.nullable).Tests
Covers the four combinations on a plain object schema:
nullable:true→ nullablenullable:true→ nullableNotes
OpenAPI 3.1 nullability (type arrays /
nullintype) is out of scope — this addresses the 3.0nullablekeyword.allOf+nullableordering edge cases (a property declared in one member, marked required in a later member) are rare and handled best-effort via the OR.Closes #41
🤖 Generated with Claude Code