diff --git a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs index a9606bb0..818b2063 100644 --- a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs @@ -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 @@ -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 diff --git a/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs index a38c9203..b260e19b 100644 --- a/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.V3SchemaCompilationTests.fs @@ -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" } + } + } + ] + } + } + } +}""" + +[] +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 + +[] +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 + + dogType.GetDeclaredProperty("Breed").PropertyType + |> shouldEqual typeof + +// ── nullable required property → option type ───────────────────────────────── + +[] +let ``required nullable property compiles to option type``() = + // In OpenAPI 3.0, a required + nullable property must be Option + // 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 + +// ── additionalProperties → Map ──────────────────────────────────── + +/// OpenAPI 3.0 schema where StringMap has only additionalProperties (no explicit properties). +/// The compiler releases the name reservation and compiles it to Map. +/// Any property referencing StringMap by $ref should receive type Map. +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" } + } + } + } + } +}""" + +[] +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 + +[] +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>