diff --git a/src/OpenApiCodeGenerator/CSharpCodeEmitter.cs b/src/OpenApiCodeGenerator/CSharpCodeEmitter.cs
index 3903097..7ec2cae 100644
--- a/src/OpenApiCodeGenerator/CSharpCodeEmitter.cs
+++ b/src/OpenApiCodeGenerator/CSharpCodeEmitter.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Text.Json.Nodes;
@@ -168,7 +169,27 @@ private static bool HasValidationConstraints(IOpenApiSchema schema)
!string.IsNullOrEmpty(schema.Minimum) ||
!string.IsNullOrEmpty(schema.Maximum) ||
schema.MinItems.HasValue ||
- schema.MaxItems.HasValue;
+ schema.MaxItems.HasValue ||
+ HasFormatValidationAttribute(schema);
+ }
+
+ ///
+ /// Checks whether the schema's string format maps to a validation attribute.
+ ///
+ private static bool HasFormatValidationAttribute(IOpenApiSchema schema)
+ {
+ return GetFormatValidationAttribute(schema.Format) != null;
+ }
+
+ [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Intended")]
+ private static string? GetFormatValidationAttribute(string? format)
+ {
+ return format?.ToLowerInvariant() switch
+ {
+ "email" => "[EmailAddress]",
+ "phone" => "[Phone]",
+ _ => null
+ };
}
private void EmitTypeAliasInterface()
@@ -1497,6 +1518,16 @@ private void EmitValidationAttributes(IOpenApiSchema schema)
bool isArray = TypeResolver.HasTypeFlag(schema, JsonSchemaType.Array);
bool isNumber = TypeResolver.HasTypeFlag(schema, JsonSchemaType.Number) || TypeResolver.HasTypeFlag(schema, JsonSchemaType.Integer);
+ // Format-based validation attributes for strings
+ if (isString)
+ {
+ string? formatAttr = GetFormatValidationAttribute(schema.Format);
+ if (formatAttr != null)
+ {
+ AppendLine(formatAttr);
+ }
+ }
+
if (isString)
{
int? minLength = schema.MinLength;
diff --git a/tests/OpenApiCodeGenerator.Tests/CSharpCodeEmitterTests.cs b/tests/OpenApiCodeGenerator.Tests/CSharpCodeEmitterTests.cs
index b1af185..fc9e04a 100644
--- a/tests/OpenApiCodeGenerator.Tests/CSharpCodeEmitterTests.cs
+++ b/tests/OpenApiCodeGenerator.Tests/CSharpCodeEmitterTests.cs
@@ -3183,6 +3183,159 @@ await File.WriteAllTextAsync(Path.Combine(tempRoot, "Harness.csproj"), """
#endregion
+ #region Format-Based Validation Attributes
+
+ [Fact]
+ public void Emit_StringWithEmailFormat_EmitsEmailAddressAttribute()
+ {
+ var schemas = new Dictionary
+ {
+ ["User"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" }
+ }
+ }
+ };
+
+ string result = Generate(schemas);
+
+ Assert.Contains("[EmailAddress]", result, StringComparison.Ordinal);
+ Assert.Contains("using System.ComponentModel.DataAnnotations;", result, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Emit_StringWithPhoneFormat_EmitsPhoneAttribute()
+ {
+ var schemas = new Dictionary
+ {
+ ["Contact"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["phone"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "phone" }
+ }
+ }
+ };
+
+ string result = Generate(schemas);
+
+ Assert.Contains("[Phone]", result, StringComparison.Ordinal);
+ Assert.Contains("using System.ComponentModel.DataAnnotations;", result, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Emit_StringWithEmailFormat_WithValidationDisabled_DoesNotEmitEmailAddress()
+ {
+ var schemas = new Dictionary
+ {
+ ["User"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" }
+ }
+ }
+ };
+
+ string result = Generate(schemas, new GeneratorOptions
+ {
+ GenerateFileHeader = false,
+ Namespace = "TestModels",
+ EmitValidationAttributes = false
+ });
+
+ Assert.DoesNotContain("[EmailAddress]", result, StringComparison.Ordinal);
+ Assert.DoesNotContain("System.ComponentModel.DataAnnotations", result, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Emit_StringWithUnknownFormat_DoesNotEmitFormatValidationAttribute()
+ {
+ var schemas = new Dictionary
+ {
+ ["User"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["hostname"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "hostname" }
+ }
+ }
+ };
+
+ string result = Generate(schemas);
+
+ // hostname is not a recognized format — no validation attribute
+ Assert.DoesNotContain("[EmailAddress]", result, StringComparison.Ordinal);
+ Assert.DoesNotContain("[Phone]", result, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task Emit_EmailAndPhoneFormats_CompilesSuccessfully()
+ {
+ var schemas = new Dictionary
+ {
+ ["Contact"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" },
+ ["phone"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "phone" }
+ }
+ }
+ };
+
+ string result = Generate(schemas);
+
+ string tempRoot = Path.Combine(
+ AppContext.BaseDirectory, "..", "..", "..", "..",
+ "TestResults", "FormatValidationCompile", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempRoot);
+ try
+ {
+ await File.WriteAllTextAsync(Path.Combine(tempRoot, "Generated.cs"), result);
+ await File.WriteAllTextAsync(Path.Combine(tempRoot, "Harness.csproj"), """
+
+
+ net10.0
+ enable
+ All
+ true
+
+
+ """);
+ using var proc = new System.Diagnostics.Process();
+ proc.StartInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"build \"{Path.Combine(tempRoot, "Harness.csproj")}\" -v q --nologo",
+ WorkingDirectory = tempRoot,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ proc.Start();
+ string stdout = await proc.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken);
+ string stderr = await proc.StandardError.ReadToEndAsync(TestContext.Current.CancellationToken);
+ await proc.WaitForExitAsync(TestContext.Current.CancellationToken);
+ Assert.True(proc.ExitCode == 0,
+ $"Format validation code failed to compile.{Environment.NewLine}STDOUT:{stdout}{Environment.NewLine}STDERR:{stderr}");
+ }
+ finally
+ {
+ if (Directory.Exists(tempRoot)) Directory.Delete(tempRoot, recursive: true);
+ }
+ }
+
+ #endregion
+
#region Deprecated / Obsolete
[Fact]