Skip to content

Commit 0a91431

Browse files
authored
Merge pull request #18 from Nikcio-labs/feature/support-prefixing-models
feat: Support prefixing models
2 parents bd8bd2b + c793af7 commit 0a91431

14 files changed

Lines changed: 360 additions & 61 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ OPTIONS:
8383
-i, --input <path> Input OpenAPI spec file or URL
8484
-o, --output <path> Output C# file path
8585
-n, --namespace <name> C# namespace (default: GeneratedModels)
86+
--model-prefix <prefix> Prefix every generated model type name
8687
--no-doc-comments Disable XML doc comment generation
8788
--no-header Disable auto-generated file header
8889
--no-default-non-nullable Don't treat defaults as non-nullable
@@ -103,6 +104,7 @@ using OpenApiCodeGenerator;
103104
var generator = new CSharpSchemaGenerator(new GeneratorOptions
104105
{
105106
Namespace = "MyApp.Models",
107+
ModelPrefix = "Api",
106108
GenerateDocComments = true
107109
});
108110

docs/src/content/docs/guides/cli-usage.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ By default, generated code uses the `GeneratedModels` namespace. Override it wit
4343
openapi-codegen spec.yaml -o Models.cs -n MyApp.Api.Models
4444
```
4545

46+
## Prefixing Model Types
47+
48+
Use `--model-prefix` to prepend a prefix to every generated model type name:
49+
50+
```bash
51+
openapi-codegen spec.yaml -o Models.cs --model-prefix Api
52+
```
53+
54+
The prefix must start with a letter or underscore and can contain only letters, digits, or underscores.
55+
4656
## Disabling Features
4757

4858
Toggle individual features off with `--no-*` flags:

docs/src/content/docs/reference/cli.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ C# namespace for the generated types.
6464
openapi-codegen spec.yaml -o Models.cs -n MyApp.Api.Models
6565
```
6666

67+
### `--model-prefix <prefix>`
68+
69+
Prefix every generated model type name. The prefix must start with a letter or underscore and can contain only letters, digits, or underscores.
70+
71+
```bash
72+
openapi-codegen spec.yaml -o Models.cs --model-prefix Api
73+
```
74+
6775
### `--no-doc-comments`
6876

6977
Disable generation of XML documentation comments from OpenAPI descriptions.
@@ -144,6 +152,9 @@ openapi-codegen petstore.yaml -o Models.cs
144152
# Custom namespace
145153
openapi-codegen petstore.yaml -o Models.cs -n PetStore.Models
146154

155+
# Prefix generated model types
156+
openapi-codegen petstore.yaml -o Models.cs --model-prefix Api
157+
147158
# From URL with all options disabled
148159
openapi-codegen https://example.com/api.json \
149160
-o Models.cs \

src/OpenApiCodeGenerator.Cli/Program.cs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ static async Task<int> RunAsync(string[] args)
2323
string? inputPath = null;
2424
string? outputPath = null;
2525
string namespaceName = "GeneratedModels";
26+
string? modelPrefix = null;
2627
bool docComments = true;
2728
bool fileHeader = true;
2829
bool defaultNonNullable = true;
@@ -43,6 +44,9 @@ static async Task<int> RunAsync(string[] args)
4344
case "--namespace" or "-n":
4445
namespaceName = GetNextArg(args, ref i, "--namespace");
4546
break;
47+
case "--model-prefix":
48+
modelPrefix = GetNextArg(args, ref i, "--model-prefix");
49+
break;
4650
case "--no-doc-comments":
4751
docComments = false;
4852
break;
@@ -87,6 +91,28 @@ static async Task<int> RunAsync(string[] args)
8791
return 1;
8892
}
8993

94+
var options = new GeneratorOptions
95+
{
96+
Namespace = namespaceName,
97+
GenerateDocComments = docComments,
98+
GenerateFileHeader = fileHeader,
99+
ModelPrefix = modelPrefix,
100+
DefaultNonNullable = defaultNonNullable,
101+
UseImmutableArrays = immutableArrays,
102+
UseImmutableDictionaries = immutableDictionaries,
103+
AddDefaultValuesToProperties = addDefaultValuesToProperties,
104+
};
105+
106+
try
107+
{
108+
options.Validate();
109+
}
110+
catch (ArgumentException ex)
111+
{
112+
await Console.Error.WriteLineAsync($"Error: {ex.Message}").ConfigureAwait(false);
113+
return 1;
114+
}
115+
90116
// Support reading from URL
91117
Stream inputStream;
92118
HttpClient? httpClient = null;
@@ -107,22 +133,13 @@ static async Task<int> RunAsync(string[] args)
107133
await Console.Error.WriteLineAsync($"Error: Input file not found: {inputPath}").ConfigureAwait(false);
108134
return 1;
109135
}
110-
using Stream _ = inputStream = File.OpenRead(inputPath);
136+
#pragma warning disable CA2000 // Dispose objects before losing scope
137+
inputStream = File.OpenRead(inputPath);
138+
#pragma warning restore CA2000 // Dispose objects before losing scope
111139
}
112140

113141
try
114142
{
115-
var options = new GeneratorOptions
116-
{
117-
Namespace = namespaceName,
118-
GenerateDocComments = docComments,
119-
GenerateFileHeader = fileHeader,
120-
DefaultNonNullable = defaultNonNullable,
121-
UseImmutableArrays = immutableArrays,
122-
UseImmutableDictionaries = immutableDictionaries,
123-
AddDefaultValuesToProperties = addDefaultValuesToProperties,
124-
};
125-
126143
var generator = new CSharpSchemaGenerator(options);
127144
string code = generator.GenerateFromStream(inputStream);
128145

@@ -143,6 +160,11 @@ static async Task<int> RunAsync(string[] args)
143160

144161
return 0;
145162
}
163+
catch (ArgumentException ex)
164+
{
165+
await Console.Error.WriteLineAsync($"Error: {ex.Message}").ConfigureAwait(false);
166+
return 1;
167+
}
146168
catch (InvalidOperationException ex)
147169
{
148170
await Console.Error.WriteLineAsync($"Error: {ex.Message}").ConfigureAwait(false);
@@ -192,6 +214,7 @@ [output] Output file path (defaults to stdout)
192214
-i, --input <path> Input OpenAPI spec file or URL
193215
-o, --output <path> Output C# file path
194216
-n, --namespace <name> C# namespace (default: GeneratedModels)
217+
--model-prefix <prefix> Prefix every generated model type name
195218
--no-doc-comments Disable XML doc comment generation
196219
--no-header Disable auto-generated file header
197220
--no-default-non-nullable Don't treat defaults as non-nullable
@@ -203,7 +226,7 @@ [output] Output file path (defaults to stdout)
203226
204227
EXAMPLES:
205228
openapi-codegen petstore.yaml -o Models.cs
206-
openapi-codegen https://petstore3.swagger.io/api/v3/openapi.json -o PetStore.cs -n MyApp.Models
229+
openapi-codegen https://petstore3.swagger.io/api/v3/openapi.json -o PetStore.cs -n MyApp.Models --model-prefix PetStore
207230
openapi-codegen spec.yaml --mutable-arrays --mutable-dictionaries
208231
""");
209232
}

src/OpenApiCodeGenerator/CSharpCodeEmitter.cs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,11 @@ public string Emit()
132132
if (valueGroups.Count == 1)
133133
{
134134
// All inline enums with this name have identical values → single enum
135-
string resolvedName = baseName;
135+
string resolvedName = NameHelper.ApplyTypePrefix(baseName, _options.ModelPrefix);
136136
if (usedNames.Contains(resolvedName))
137137
{
138138
// Conflict with a top-level type; differentiate
139-
resolvedName = FindUniqueName(baseName, usedNames);
139+
resolvedName = FindUniqueName(resolvedName, usedNames);
140140
}
141141
usedNames.Add(resolvedName);
142142
foreach ((string SchemaName, string PropertyName, string EnumTypeName, List<string> Values, IOpenApiSchema Schema) in valueGroups[0])
@@ -314,7 +314,7 @@ private static bool ShouldEmitAsRecord(IOpenApiSchema schema)
314314

315315
private void EmitEnum(string schemaName, IOpenApiSchema schema, string? typeNameOverride = null)
316316
{
317-
string typeName = typeNameOverride ?? NameHelper.ToTypeName(schemaName);
317+
string typeName = typeNameOverride ?? NameHelper.ToTypeName(schemaName, _options.ModelPrefix);
318318

319319
EmitDocComment(schema.Description);
320320

@@ -391,7 +391,7 @@ private void EmitEnum(string schemaName, IOpenApiSchema schema, string? typeName
391391

392392
private void EmitRecord(string schemaName, IOpenApiSchema schema, string? typeNameOverride = null)
393393
{
394-
string typeName = typeNameOverride ?? NameHelper.ToTypeName(schemaName);
394+
string typeName = typeNameOverride ?? NameHelper.ToTypeName(schemaName, _options.ModelPrefix);
395395

396396
// Collect all properties (including from allOf)
397397
Dictionary<string, IOpenApiSchema> properties = CollectProperties(schema);
@@ -405,7 +405,7 @@ private void EmitRecord(string schemaName, IOpenApiSchema schema, string? typeNa
405405
OpenApiSchemaReference? refSchema = schema.AllOf.OfType<OpenApiSchemaReference>().FirstOrDefault();
406406
if (refSchema?.Reference?.Id != null)
407407
{
408-
baseType = NameHelper.ToTypeName(refSchema.Reference.Id);
408+
baseType = NameHelper.ToTypeName(refSchema.Reference.Id, _options.ModelPrefix);
409409
basePropertyNames = CollectBasePropertyNames(refSchema);
410410
}
411411
}
@@ -463,7 +463,7 @@ private void EmitProperty(string propertyName, IOpenApiSchema propertySchema, bo
463463
string typeName;
464464
if (propertySchema is OpenApiSchemaReference propRef && propRef.Reference.Id is not null)
465465
{
466-
string refTypeName = NameHelper.ToTypeName(propRef.Reference.Id);
466+
string refTypeName = NameHelper.ToTypeName(propRef.Reference.Id, _options.ModelPrefix);
467467
typeName = isRequired ? refTypeName : refTypeName + "?";
468468
}
469469
else if (TypeResolver.IsEnum(propertySchema) && !_allSchemas.Values.Contains(propertySchema))
@@ -552,19 +552,19 @@ private void EmitProperty(string propertyName, IOpenApiSchema propertySchema, bo
552552
return $"\"{strVal.Replace("\"", "\\\"", StringComparison.Ordinal)}\"";
553553
}
554554

555-
if (schema.Format == "date-time" && DateTime.TryParse(strVal, null, DateTimeStyles.AdjustToUniversal, out DateTime dt))
555+
if (schema.Format == "date-time" && DateTimeOffset.TryParse(strVal, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset dto))
556556
{
557-
return $"DateTime.Parse(\"{dt.ToString("o", CultureInfo.InvariantCulture)}\", null, DateTimeStyles.AdjustToUniversal)";
557+
return $"DateTimeOffset.Parse(\"{dto.ToString("O", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)";
558558
}
559559

560-
if (schema.Format == "date" && DateTime.TryParse(strVal, null, DateTimeStyles.None, out DateTime d))
560+
if (schema.Format == "date" && DateOnly.TryParse(strVal, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly dateOnly))
561561
{
562-
return $"DateTime.Parse(\"{d:yyyy-MM-dd}\")";
562+
return $"DateOnly.ParseExact(\"{dateOnly:yyyy-MM-dd}\", \"yyyy-MM-dd\", CultureInfo.InvariantCulture)";
563563
}
564564

565-
if (schema.Format == "time" && TimeSpan.TryParse(strVal, out TimeSpan ts))
565+
if (schema.Format == "time" && TimeOnly.TryParse(strVal, CultureInfo.InvariantCulture, DateTimeStyles.None, out TimeOnly timeOnly))
566566
{
567-
return $"TimeSpan.Parse(\"{ts:c}\")";
567+
return $"TimeOnly.Parse(\"{timeOnly.ToString("O", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)";
568568
}
569569

570570
if (schema.Format == "uuid" && Guid.TryParse(strVal, out Guid guid))
@@ -625,7 +625,7 @@ private bool IsEnumTypeName(string typeName)
625625
// Check top-level schemas for a matching enum
626626
foreach ((string? name, IOpenApiSchema? schema) in _allSchemas)
627627
{
628-
if (NameHelper.ToTypeName(name) == typeName && TypeResolver.IsEnum(schema))
628+
if (NameHelper.ToTypeName(name, _options.ModelPrefix) == typeName && TypeResolver.IsEnum(schema))
629629
{
630630
return true;
631631
}
@@ -641,7 +641,7 @@ private bool IsEnumTypeName(string typeName)
641641

642642
private void EmitTypeAlias(string schemaName, IOpenApiSchema schema, string? typeNameOverride = null)
643643
{
644-
string typeName = typeNameOverride ?? NameHelper.ToTypeName(schemaName);
644+
string typeName = typeNameOverride ?? NameHelper.ToTypeName(schemaName, _options.ModelPrefix);
645645
string resolvedType = _typeResolver.ResolveUnderlyingType(schema);
646646

647647
EmitDocComment(schema.Description);
@@ -662,7 +662,7 @@ private void EmitTypeAlias(string schemaName, IOpenApiSchema schema, string? typ
662662

663663
private void EmitUnionType(string schemaName, IOpenApiSchema schema, string? typeNameOverride = null)
664664
{
665-
string typeName = typeNameOverride ?? NameHelper.ToTypeName(schemaName);
665+
string typeName = typeNameOverride ?? NameHelper.ToTypeName(schemaName, _options.ModelPrefix);
666666
IList<IOpenApiSchema> variants = schema.OneOf ?? schema.AnyOf ?? [];
667667

668668
EmitDocComment(schema.Description);
@@ -690,7 +690,7 @@ private void EmitDiscriminatedUnion(
690690
{
691691
foreach ((string? key, OpenApiSchemaReference? schemaRef) in discriminator.Mapping)
692692
{
693-
mapping[key] = NameHelper.ToTypeName(schemaRef.Reference.Id);
693+
mapping[key] = NameHelper.ToTypeName(schemaRef.Reference.Id, _options.ModelPrefix);
694694
}
695695
}
696696
else
@@ -707,7 +707,7 @@ private void EmitDiscriminatedUnion(
707707
continue;
708708
}
709709

710-
mapping[name] = NameHelper.ToTypeName(name);
710+
mapping[name] = NameHelper.ToTypeName(name, _options.ModelPrefix);
711711
}
712712
}
713713
}
@@ -728,7 +728,7 @@ private void EmitSimpleUnion(string typeName, IList<IOpenApiSchema> variants)
728728
// Collect the variant type names for documentation
729729
var variantNames = variants
730730
.OfType<OpenApiSchemaReference>()
731-
.Select(v => NameHelper.ToTypeName(v.Reference.Id))
731+
.Select(v => NameHelper.ToTypeName(v.Reference.Id, _options.ModelPrefix))
732732
.ToList();
733733

734734
if (variantNames.Count > 0)
@@ -743,7 +743,7 @@ private void EmitSimpleUnion(string typeName, IList<IOpenApiSchema> variants)
743743
{
744744
foreach (OpenApiSchemaReference variant in variants.OfType<OpenApiSchemaReference>())
745745
{
746-
string derivedName = NameHelper.ToTypeName(variant.Reference.Id);
746+
string derivedName = NameHelper.ToTypeName(variant.Reference.Id, _options.ModelPrefix);
747747
AppendLine($"[JsonDerivedType(typeof({derivedName}), \"{variant.Reference.Id}\")]");
748748
}
749749
}
@@ -761,14 +761,14 @@ private void EmitSimpleUnion(string typeName, IList<IOpenApiSchema> variants)
761761
/// In each collision group, the most "natural" schema name keeps the clean PascalCase
762762
/// type name, and others get meaningfully differentiated names.
763763
/// </summary>
764-
private static Dictionary<string, string> ResolveTypeNameCollisions(IEnumerable<string> schemaNames)
764+
private Dictionary<string, string> ResolveTypeNameCollisions(IEnumerable<string> schemaNames)
765765
{
766766
var result = new Dictionary<string, string>();
767767
var usedNames = new HashSet<string>(StringComparer.Ordinal);
768768

769769
// Group schema names by their PascalCase type name
770770
var groups = schemaNames
771-
.Select(name => (Original: name, PascalName: NameHelper.ToTypeName(name)))
771+
.Select(name => (Original: name, PascalName: NameHelper.ToTypeName(name, prefix: null)))
772772
.GroupBy(x => x.PascalName)
773773
.ToList();
774774

@@ -779,7 +779,7 @@ private static Dictionary<string, string> ResolveTypeNameCollisions(IEnumerable<
779779

780780
if (members.Count == 1)
781781
{
782-
result[members[0].Original] = pascalName;
782+
result[members[0].Original] = NameHelper.ApplyTypePrefix(pascalName, _options.ModelPrefix);
783783
usedNames.Add(pascalName);
784784
continue;
785785
}
@@ -788,14 +788,14 @@ private static Dictionary<string, string> ResolveTypeNameCollisions(IEnumerable<
788788
var sorted = members.OrderBy(m => NameHelper.NaturalnessScore(m.Original, pascalName)).ToList();
789789

790790
// Winner gets the clean name
791-
result[sorted[0].Original] = pascalName;
791+
result[sorted[0].Original] = NameHelper.ApplyTypePrefix(pascalName, _options.ModelPrefix);
792792
usedNames.Add(pascalName);
793793

794794
// Others get differentiated names
795795
for (int i = 1; i < sorted.Count; i++)
796796
{
797797
string differentiated = NameHelper.ToDifferentiatedName(sorted[i].Original, pascalName, usedNames);
798-
result[sorted[i].Original] = differentiated;
798+
result[sorted[i].Original] = NameHelper.ApplyTypePrefix(differentiated, _options.ModelPrefix);
799799
usedNames.Add(differentiated);
800800
}
801801
}

src/OpenApiCodeGenerator/CSharpSchemaGenerator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public CSharpSchemaGenerator() : this(new GeneratorOptions()) { }
1515

1616
public CSharpSchemaGenerator(GeneratorOptions options)
1717
{
18+
ArgumentNullException.ThrowIfNull(options);
19+
options.Validate();
1820
_options = options;
1921
}
2022

0 commit comments

Comments
 (0)