From e061e121c54db1ae421578777784342674a8ed17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 08:03:10 +0000 Subject: [PATCH 1/6] Initial plan From 8ce7259d49c8309b9680fc522651062740ac6f26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 08:33:34 +0000 Subject: [PATCH 2/6] Fix int32 format with pattern quirk - infer integer type from format Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- .../CSharpClientGeneratorFactory.cs | 95 +++++++++++++++++++ .../Examples/Int32FormatWithPatternTests.cs | 93 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index cd86d2d5d..d5babf008 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -19,6 +19,7 @@ public CustomCSharpClientGenerator Create() } } + FixMissingTypesWithIntegerFormat(); ApplyCustomIntegerType(); var csharpClientGeneratorSettings = new CSharpClientGeneratorSettings @@ -64,6 +65,100 @@ public CustomCSharpClientGenerator Create() return generator; } + private void FixMissingTypesWithIntegerFormat() + { + ProcessSchemasForMissingTypes(document.Components.Schemas); + + foreach (var path in document.Paths) + { + if (path.Value == null) continue; + + foreach (var operation in path.Value.Values) + { + if (operation == null) continue; + + foreach (var parameter in operation.Parameters) + { + FixSchemaTypeFromFormat(parameter.ActualSchema); + } + + if (operation.RequestBody?.Content != null) + { + foreach (var content in operation.RequestBody.Content.Values) + { + ProcessSchemaForMissingTypes(content.Schema); + } + } + + foreach (var response in operation.Responses.Values) + { + if (response.Content != null) + { + foreach (var content in response.Content.Values) + { + ProcessSchemaForMissingTypes(content.Schema); + } + } + } + } + } + } + + private void ProcessSchemasForMissingTypes(IDictionary schemas) + { + foreach (var schema in schemas.Values) + { + ProcessSchemaForMissingTypes(schema); + } + } + + private void ProcessSchemaForMissingTypes(JsonSchema? schema) + { + if (schema == null) return; + + var actualSchema = schema.ActualSchema; + + FixSchemaTypeFromFormat(actualSchema); + + foreach (var property in actualSchema.Properties.Values) + { + ProcessSchemaForMissingTypes(property); + } + + if (actualSchema.Item != null) + { + ProcessSchemaForMissingTypes(actualSchema.Item); + } + + if (actualSchema.AdditionalPropertiesSchema != null) + { + ProcessSchemaForMissingTypes(actualSchema.AdditionalPropertiesSchema); + } + + var subSchemas = actualSchema.AllOf + .Concat(actualSchema.OneOf) + .Concat(actualSchema.AnyOf) + .ToArray(); + + foreach (var subSchema in subSchemas) + { + ProcessSchemaForMissingTypes(subSchema); + } + } + + private void FixSchemaTypeFromFormat(JsonSchema schema) + { + // If type is not set but format indicates an integer type, set the type to Integer + if ((schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null) && + !string.IsNullOrEmpty(schema.Format)) + { + if (schema.Format == "int32" || schema.Format == "int64") + { + schema.Type = JsonObjectType.Integer; + } + } + } + private void ApplyCustomIntegerType() { var customIntegerType = settings.CodeGeneratorSettings?.IntegerType ?? IntegerType.Int32; diff --git a/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs new file mode 100644 index 000000000..1c3787a16 --- /dev/null +++ b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs @@ -0,0 +1,93 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Refitter.Tests.TestUtilities; +using TUnit.Core; + +namespace Refitter.Tests.Examples; + +public class Int32FormatWithPatternTests +{ + private const string OpenApiSpec = @" +openapi: 3.0.4 +info: + version: '1.0.0' + title: 'WeatherForecast API' +servers: + - url: 'https://api.example.com/v1' +paths: + /weatherforecast: + get: + operationId: 'getWeatherForecast' + responses: + '200': + description: 'Success' + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/WeatherForecast' +components: + schemas: + WeatherForecast: + type: object + properties: + date: + type: string + format: date + temperatureC: + pattern: '^-?(?:0|[1-9]\\d*)$' + format: int32 + temperatureF: + pattern: '^-?(?:0|[1-9]\\d*)$' + format: int32 + summary: + type: string + nullable: true +"; + + [Test] + public async Task Can_Generate_Code() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().NotBeNullOrWhiteSpace(); + } + + [Test] + public async Task Should_Generate_Int_For_Properties_With_Format_Int32_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate int, not object + generatedCode.Should().Contain("public int TemperatureC { get; set; }"); + generatedCode.Should().Contain("public int TemperatureF { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object TemperatureC { get; set; }"); + generatedCode.Should().NotContain("public object TemperatureF { get; set; }"); + } + + [Test] + public async Task Can_Build_Generated_Code() + { + string generatedCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generatedCode) + .Should() + .BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generatedCode = sut.Generate(); + return generatedCode; + } +} From 2c6327f4bdd2511733b99f654018cf1bd8e963d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 08:34:44 +0000 Subject: [PATCH 3/6] Apply code formatting - line endings normalization Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs index 1c3787a16..53c8918f4 100644 --- a/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs +++ b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs @@ -58,11 +58,11 @@ public async Task Can_Generate_Code() public async Task Should_Generate_Int_For_Properties_With_Format_Int32_And_Pattern() { string generatedCode = await GenerateCode(); - + // Should generate int, not object generatedCode.Should().Contain("public int TemperatureC { get; set; }"); generatedCode.Should().Contain("public int TemperatureF { get; set; }"); - + // Should not generate as object generatedCode.Should().NotContain("public object TemperatureC { get; set; }"); generatedCode.Should().NotContain("public object TemperatureF { get; set; }"); From 21a05c3a7d42fbf8b8da676263bd208e2bffd5f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:43:06 +0000 Subject: [PATCH 4/6] Add support for float and double formats with pattern - extend numeric type inference Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- .../CSharpClientGeneratorFactory.cs | 6 +- .../Examples/Int32FormatWithPatternTests.cs | 103 ++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index d5babf008..d1937c00b 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -148,7 +148,7 @@ private void ProcessSchemaForMissingTypes(JsonSchema? schema) private void FixSchemaTypeFromFormat(JsonSchema schema) { - // If type is not set but format indicates an integer type, set the type to Integer + // If type is not set but format indicates a numeric type, set the type based on format if ((schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null) && !string.IsNullOrEmpty(schema.Format)) { @@ -156,6 +156,10 @@ private void FixSchemaTypeFromFormat(JsonSchema schema) { schema.Type = JsonObjectType.Integer; } + else if (schema.Format == "float" || schema.Format == "double") + { + schema.Type = JsonObjectType.Number; + } } } diff --git a/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs index 53c8918f4..af5e0a3d0 100644 --- a/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs +++ b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs @@ -91,3 +91,106 @@ private static async Task GenerateCode() return generatedCode; } } + +public class NumericFormatWithPatternTests +{ + private const string OpenApiSpec = @" +openapi: 3.0.4 +info: + version: '1.0.0' + title: 'Sensor Data API' +servers: + - url: 'https://api.example.com/v1' +paths: + /sensor: + get: + operationId: 'getSensorData' + responses: + '200': + description: 'Success' + content: + application/json: + schema: + $ref: '#/components/schemas/SensorData' +components: + schemas: + SensorData: + type: object + properties: + temperature: + pattern: '^-?\\d+\\.\\d+$' + format: float + humidity: + pattern: '^-?\\d+\\.\\d+$' + format: double + pressure: + pattern: '^-?(?:0|[1-9]\\d*)$' + format: int64 +"; + + [Test] + public async Task Can_Generate_Code() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().NotBeNullOrWhiteSpace(); + } + + [Test] + public async Task Should_Generate_Float_For_Properties_With_Format_Float_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate float, not object + generatedCode.Should().Contain("public float Temperature { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Temperature { get; set; }"); + } + + [Test] + public async Task Should_Generate_Double_For_Properties_With_Format_Double_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate double, not object + generatedCode.Should().Contain("public double Humidity { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Humidity { get; set; }"); + } + + [Test] + public async Task Should_Generate_Long_For_Properties_With_Format_Int64_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate long, not object + generatedCode.Should().Contain("public long Pressure { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Pressure { get; set; }"); + } + + [Test] + public async Task Can_Build_Generated_Code() + { + string generatedCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generatedCode) + .Should() + .BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generatedCode = sut.Generate(); + return generatedCode; + } +} From 5373ccb6c662b6e13390dc08e6079e6b7107a9a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:44:09 +0000 Subject: [PATCH 5/6] Update comment to be more specific about handled formats Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- src/Refitter.Core/CSharpClientGeneratorFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index d1937c00b..279b69585 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -148,7 +148,7 @@ private void ProcessSchemaForMissingTypes(JsonSchema? schema) private void FixSchemaTypeFromFormat(JsonSchema schema) { - // If type is not set but format indicates a numeric type, set the type based on format + // If type is not set but format indicates a numeric type (int32, int64, float, double), set the type based on format if ((schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null) && !string.IsNullOrEmpty(schema.Format)) { From 8ea8a21f06eb170c6e91317f0bad585119109355 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 26 Jan 2026 14:26:29 +0100 Subject: [PATCH 6/6] Refactor numeric format tests - move tests to NumericFormatWithPatternTests and clean up Int32FormatWithPatternTests --- .../Examples/Int32FormatWithPatternTests.cs | 103 ---------------- .../Examples/NumericFormatWithPatternTests.cs | 110 ++++++++++++++++++ 2 files changed, 110 insertions(+), 103 deletions(-) create mode 100644 src/Refitter.Tests/Examples/NumericFormatWithPatternTests.cs diff --git a/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs index af5e0a3d0..53c8918f4 100644 --- a/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs +++ b/src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs @@ -91,106 +91,3 @@ private static async Task GenerateCode() return generatedCode; } } - -public class NumericFormatWithPatternTests -{ - private const string OpenApiSpec = @" -openapi: 3.0.4 -info: - version: '1.0.0' - title: 'Sensor Data API' -servers: - - url: 'https://api.example.com/v1' -paths: - /sensor: - get: - operationId: 'getSensorData' - responses: - '200': - description: 'Success' - content: - application/json: - schema: - $ref: '#/components/schemas/SensorData' -components: - schemas: - SensorData: - type: object - properties: - temperature: - pattern: '^-?\\d+\\.\\d+$' - format: float - humidity: - pattern: '^-?\\d+\\.\\d+$' - format: double - pressure: - pattern: '^-?(?:0|[1-9]\\d*)$' - format: int64 -"; - - [Test] - public async Task Can_Generate_Code() - { - string generatedCode = await GenerateCode(); - generatedCode.Should().NotBeNullOrWhiteSpace(); - } - - [Test] - public async Task Should_Generate_Float_For_Properties_With_Format_Float_And_Pattern() - { - string generatedCode = await GenerateCode(); - - // Should generate float, not object - generatedCode.Should().Contain("public float Temperature { get; set; }"); - - // Should not generate as object - generatedCode.Should().NotContain("public object Temperature { get; set; }"); - } - - [Test] - public async Task Should_Generate_Double_For_Properties_With_Format_Double_And_Pattern() - { - string generatedCode = await GenerateCode(); - - // Should generate double, not object - generatedCode.Should().Contain("public double Humidity { get; set; }"); - - // Should not generate as object - generatedCode.Should().NotContain("public object Humidity { get; set; }"); - } - - [Test] - public async Task Should_Generate_Long_For_Properties_With_Format_Int64_And_Pattern() - { - string generatedCode = await GenerateCode(); - - // Should generate long, not object - generatedCode.Should().Contain("public long Pressure { get; set; }"); - - // Should not generate as object - generatedCode.Should().NotContain("public object Pressure { get; set; }"); - } - - [Test] - public async Task Can_Build_Generated_Code() - { - string generatedCode = await GenerateCode(); - BuildHelper - .BuildCSharp(generatedCode) - .Should() - .BeTrue(); - } - - private static async Task GenerateCode() - { - var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); - var settings = new RefitGeneratorSettings - { - OpenApiPath = swaggerFile, - }; - - var sut = await RefitGenerator.CreateAsync(settings); - var generatedCode = sut.Generate(); - return generatedCode; - } -} diff --git a/src/Refitter.Tests/Examples/NumericFormatWithPatternTests.cs b/src/Refitter.Tests/Examples/NumericFormatWithPatternTests.cs new file mode 100644 index 000000000..fa9edddfd --- /dev/null +++ b/src/Refitter.Tests/Examples/NumericFormatWithPatternTests.cs @@ -0,0 +1,110 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Refitter.Tests.TestUtilities; + +namespace Refitter.Tests.Examples +{ + public class NumericFormatWithPatternTests + { + private const string OpenApiSpec = @" +openapi: 3.0.4 +info: + version: '1.0.0' + title: 'Sensor Data API' +servers: + - url: 'https://api.example.com/v1' +paths: + /sensor: + get: + operationId: 'getSensorData' + responses: + '200': + description: 'Success' + content: + application/json: + schema: + $ref: '#/components/schemas/SensorData' +components: + schemas: + SensorData: + type: object + properties: + temperature: + pattern: '^-?\\d+\\.\\d+$' + format: float + humidity: + pattern: '^-?\\d+\\.\\d+$' + format: double + pressure: + pattern: '^-?(?:0|[1-9]\\d*)$' + format: int64 +"; + + [Test] + public async Task Can_Generate_Code() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().NotBeNullOrWhiteSpace(); + } + + [Test] + public async Task Should_Generate_Float_For_Properties_With_Format_Float_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate float, not object + generatedCode.Should().Contain("public float Temperature { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Temperature { get; set; }"); + } + + [Test] + public async Task Should_Generate_Double_For_Properties_With_Format_Double_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate double, not object + generatedCode.Should().Contain("public double Humidity { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Humidity { get; set; }"); + } + + [Test] + public async Task Should_Generate_Long_For_Properties_With_Format_Int64_And_Pattern() + { + string generatedCode = await GenerateCode(); + + // Should generate long, not object + generatedCode.Should().Contain("public long Pressure { get; set; }"); + + // Should not generate as object + generatedCode.Should().NotContain("public object Pressure { get; set; }"); + } + + [Test] + public async Task Can_Build_Generated_Code() + { + string generatedCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generatedCode) + .Should() + .BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generatedCode = sut.Generate(); + return generatedCode; + } + } +}