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
99 changes: 99 additions & 0 deletions src/Refitter.Core/CSharpClientGeneratorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public CustomCSharpClientGenerator Create()
}
}

FixMissingTypesWithIntegerFormat();
Comment thread
christianhelle marked this conversation as resolved.
ApplyCustomIntegerType();

var csharpClientGeneratorSettings = new CSharpClientGeneratorSettings
Expand Down Expand Up @@ -64,6 +65,104 @@ 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);
}
}
}
Comment on lines +93 to +102

Copilot AI Jan 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
}
Comment on lines +76 to +103

Copilot AI Jan 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
}
Comment on lines +72 to +104

Copilot AI Jan 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
}

private void ProcessSchemasForMissingTypes(IDictionary<string, JsonSchema> 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 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))
{
if (schema.Format == "int32" || schema.Format == "int64")
Comment thread
christianhelle marked this conversation as resolved.
{
schema.Type = JsonObjectType.Integer;
}
else if (schema.Format == "float" || schema.Format == "double")
{
schema.Type = JsonObjectType.Number;
}
}
}
Comment on lines +149 to +164

Copilot AI Jan 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format string comparisons should use case-insensitive comparison or be normalized to lowercase. The OpenAPI specification doesn't mandate specific casing for format values, and some generators might use different cases (e.g., "Float" vs "float"). Consider using schema.Format.Equals("int32", StringComparison.OrdinalIgnoreCase) for robustness.

Copilot uses AI. Check for mistakes.

private void ApplyCustomIntegerType()
{
var customIntegerType = settings.CodeGeneratorSettings?.IntegerType ?? IntegerType.Int32;
Expand Down
93 changes: 93 additions & 0 deletions src/Refitter.Tests/Examples/Int32FormatWithPatternTests.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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;
}
}
110 changes: 110 additions & 0 deletions src/Refitter.Tests/Examples/NumericFormatWithPatternTests.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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;
}
}
}
Loading