From 4ebaa08021e2dfdbd38aa9a43e02e3bc79292dee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:05:21 +0000 Subject: [PATCH 1/2] fix: resolve oneOf/anyOf with single $ref to referenced type (#243) When a schema uses `oneOf: [$ref]` or `anyOf: [$ref]` with a single entry, resolve to the referenced type instead of falling through to `obj`. This pattern is common in OpenAPI specs generated by tools (e.g. NSwag, Kiota, AutoRest) and in OpenAPI 3.1 schemas that use oneOf/anyOf for nullable type annotations. Previously only `allOf: [$ref]` (the OpenAPI 3.0 canonical form) was handled; the equivalent oneOf and anyOf forms produced `obj` instead of the referenced type. Changes: - DefinitionCompiler.fs: add oneOf/anyOf single-ref handlers after the existing allOf single-ref handler; also extend the type-inference guard to check oneOf/anyOf single-entry type when schemaObj.Type is missing - Schema.TypeMappingTests.fs: add 6 tests covering oneOf and anyOf with primitive and object aliases, required and optional variants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../v3/DefinitionCompiler.fs | 46 ++++++++- .../v3/Schema.TypeMappingTests.fs | 94 +++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs index 76617de5..306a344e 100644 --- a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs @@ -410,7 +410,7 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b ty :> Type let resolvedType = - // If schemaObj.Type is missing, but allOf is present andallOf subschema has one element, use that + // If schemaObj.Type is missing, but allOf/oneOf/anyOf is present with one subschema, use that if not schemaObj.Type.HasValue && not(isNull schemaObj.AllOf) @@ -422,6 +422,28 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b Some firstAllOf.Type.Value else None + else if + not schemaObj.Type.HasValue + && not(isNull schemaObj.OneOf) + && schemaObj.OneOf.Count = 1 + then + let firstOneOf = schemaObj.OneOf.[0] + + if not(isNull firstOneOf) && firstOneOf.Type.HasValue then + Some firstOneOf.Type.Value + else + None + else if + not schemaObj.Type.HasValue + && not(isNull schemaObj.AnyOf) + && schemaObj.AnyOf.Count = 1 + then + let firstAnyOf = schemaObj.AnyOf.[0] + + if not(isNull firstAnyOf) && firstAnyOf.Type.HasValue then + Some firstAnyOf.Type.Value + else + None else if schemaObj.Type.HasValue then Some schemaObj.Type.Value else @@ -475,6 +497,28 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b ns.ReleaseNameReservation tyName compileByPath <| getFullPath schemaRef.Reference.Id | _ -> compileNewObject() + // Handle oneOf with single reference (resolves to the referenced type) + | _ when + not(isNull schemaObj.OneOf) + && schemaObj.OneOf.Count = 1 + && (schemaObj.Properties |> isNull || schemaObj.Properties.Count = 0) + -> + match schemaObj.OneOf.[0] with + | :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) -> + ns.ReleaseNameReservation tyName + compileByPath <| getFullPath schemaRef.Reference.Id + | _ -> compileNewObject() + // Handle anyOf with single reference (resolves to the referenced type) + | _ when + not(isNull schemaObj.AnyOf) + && schemaObj.AnyOf.Count = 1 + && (schemaObj.Properties |> isNull || schemaObj.Properties.Count = 0) + -> + match schemaObj.AnyOf.[0] with + | :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) -> + ns.ReleaseNameReservation tyName + compileByPath <| getFullPath schemaRef.Reference.Id + | _ -> compileNewObject() | _ when resolvedType.IsNone || resolvedType = Some JsonSchemaType.Object diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs index 072937f7..b6d41e59 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs @@ -256,6 +256,100 @@ let ``allOf $ref to integer alias resolves to int32``() = let ty = compileAllOfRefType " type: integer\n" true ty |> shouldEqual typeof +// ── $ref to primitive-type alias (via oneOf wrapper) ───────────────────────── +// oneOf: [$ref] with a single entry is semantically equivalent to a direct $ref. +// Some code generators (e.g., NSwag, Kiota) emit this form. + +/// Compile a schema where `TestType.Value` uses `oneOf: [$ref]` to reference a component alias. +let private compileOneOfRefType (aliasYaml: string) (required: bool) : Type = + let requiredBlock = + if required then + " required:\n - Value\n" + else + "" + + let schemaStr = + sprintf + """openapi: "3.0.0" +info: + title: OneOfRefAliasTest + version: "1.0.0" +paths: {} +components: + schemas: + AliasType: +%s TestType: + type: object +%s properties: + Value: + oneOf: + - $ref: '#/components/schemas/AliasType' +""" + (aliasYaml.TrimEnd() + "\n") + requiredBlock + + compileSchemaAndGetValueType schemaStr + +/// Compile a schema where `TestType.Value` uses `anyOf: [$ref]` to reference a component alias. +let private compileAnyOfRefType (aliasYaml: string) (required: bool) : Type = + let requiredBlock = + if required then + " required:\n - Value\n" + else + "" + + let schemaStr = + sprintf + """openapi: "3.0.0" +info: + title: AnyOfRefAliasTest + version: "1.0.0" +paths: {} +components: + schemas: + AliasType: +%s TestType: + type: object +%s properties: + Value: + anyOf: + - $ref: '#/components/schemas/AliasType' +""" + (aliasYaml.TrimEnd() + "\n") + requiredBlock + + compileSchemaAndGetValueType schemaStr + +[] +let ``oneOf $ref to string alias resolves to string``() = + let ty = compileOneOfRefType " type: string\n" true + ty |> shouldEqual typeof + +[] +let ``oneOf $ref to integer alias resolves to int32``() = + let ty = compileOneOfRefType " type: integer\n" true + ty |> shouldEqual typeof + +[] +let ``anyOf $ref to string alias resolves to string``() = + let ty = compileAnyOfRefType " type: string\n" true + ty |> shouldEqual typeof + +[] +let ``anyOf $ref to integer alias resolves to int32``() = + let ty = compileAnyOfRefType " type: integer\n" true + ty |> shouldEqual typeof + +[] +let ``optional oneOf $ref to integer alias resolves to Option``() = + let ty = compileOneOfRefType " type: integer\n" false + ty |> shouldEqual typeof + +[] +let ``optional anyOf $ref to integer alias resolves to Option``() = + let ty = compileAnyOfRefType " type: integer\n" false + ty |> shouldEqual typeof + // ── Optional $ref to primitive-type alias ───────────────────────────────────── // When a $ref/allOf alias property is non-required, value types must be wrapped // in Option consistent with the behaviour of ordinary optional primitive properties. From dbf52f057b4f2ae5c27841054120af0c0e53e0ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 12 Apr 2026 22:05:24 +0000 Subject: [PATCH 2/2] ci: trigger checks