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
11 changes: 6 additions & 5 deletions src/SwaggerProvider.DesignTime/DefinitionCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,11 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b
let properties = schemaObj.Properties |> toSeq
let allOf = schemaObj.AllOf |> toSeq

if Seq.isEmpty properties && Seq.isEmpty allOf then
// Hoist isEmpty checks so neither sequence is evaluated more than once.
let propertiesEmpty = Seq.isEmpty properties
let allOfEmpty = Seq.isEmpty allOf

if propertiesEmpty && allOfEmpty then
if not <| isNull tyName then
ns.MarkTypeAsNameAlias tyName

Expand All @@ -280,10 +284,7 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b
if not(String.IsNullOrWhiteSpace schemaObj.Description) then
ty.AddXmlDoc schemaObj.Description

// Combine composite schemas
// Cache the isEmpty check to avoid iterating `allOf` twice (once per field/required block).
let allOfEmpty = Seq.isEmpty allOf

// Combine composite schemas: collect properties and required from all allOf schemas.
let schemaObjProperties =
let getProps(s: IOpenApiSchema) =
s.Properties |> toSeq
Expand Down
125 changes: 125 additions & 0 deletions tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,128 @@ let ``object schema description is surfaced as XmlDoc``() =
&& a.ConstructorArguments.[0].Value :? string
&& (a.ConstructorArguments.[0].Value :?> string).Contains("A widget with a name"))
|> shouldEqual true

// ── allOf composite with multiple inline schemas ──────────────────────────────

/// OpenAPI 3.0 schema where Dog uses allOf to merge two inline objects.
/// Both inline schemas contribute properties; the compiler should emit all of them.
let private allOfCompositeSchema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Dog": {
"type": "object",
"allOf": [
{
"type": "object",
"properties": {
"name": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"breed": { "type": "string" }
}
}
]
}
}
}
}"""

[<Fact>]
let ``allOf composite with multiple inline schemas emits all merged properties``() =
let types = compileV3Schema allOfCompositeSchema false
let dogType = types |> List.find(fun t -> t.Name = "Dog")
dogType.GetDeclaredProperty("Name") |> isNull |> shouldEqual false
dogType.GetDeclaredProperty("Breed") |> isNull |> shouldEqual false

[<Fact>]
let ``allOf composite merged properties have correct types``() =
let types = compileV3Schema allOfCompositeSchema false
let dogType = types |> List.find(fun t -> t.Name = "Dog")

dogType.GetDeclaredProperty("Name").PropertyType
|> shouldEqual typeof<string option>

dogType.GetDeclaredProperty("Breed").PropertyType
|> shouldEqual typeof<string option>

// ── nullable required property β†’ option type ─────────────────────────────────

[<Fact>]
let ``required nullable property compiles to option type``() =
// In OpenAPI 3.0, a required + nullable property must be Option<T>
// because the value may be present but null.
let schema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Status": {
"type": "object",
"required": ["code"],
"properties": {
"code": { "type": "string", "nullable": true }
}
}
}
}
}"""

let types = compileV3Schema schema false
let statusType = types |> List.find(fun t -> t.Name = "Status")

statusType.GetDeclaredProperty("Code").PropertyType
|> shouldEqual typeof<string option>

// ── additionalProperties β†’ Map<string, T> ────────────────────────────────────

/// OpenAPI 3.0 schema where StringMap has only additionalProperties (no explicit properties).
/// The compiler releases the name reservation and compiles it to Map<string, string>.
/// Any property referencing StringMap by $ref should receive type Map<string, string>.
let private additionalPropertiesSchema =
"""{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"StringMap": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"Wrapper": {
"type": "object",
"properties": {
"data": { "$ref": "#/components/schemas/StringMap" }
}
}
}
}
}"""

[<Fact>]
let ``schema with only additionalProperties does not emit a named type``() =
let types = compileV3Schema additionalPropertiesSchema false
// StringMap's name reservation is released; no separate named type is emitted
types
|> List.exists(fun t -> t.Name = "StringMap")
|> shouldEqual false

[<Fact>]
let ``property referencing an additionalProperties schema has Map type``() =
let types = compileV3Schema additionalPropertiesSchema false
let wrapperType = types |> List.find(fun t -> t.Name = "Wrapper")
let dataProp = wrapperType.GetDeclaredProperty("Data")
dataProp |> isNull |> shouldEqual false
// Map types are left unwrapped (not option) for non-required properties;
// collection types naturally express absence via null/empty.
let propType = dataProp.PropertyType
propType |> shouldEqual typeof<Map<string, string>>
Loading