Records, not classes
Immutable record types with init-only properties. The spec says a property is required? You get the required keyword. Everything else is honestly nullable.

# One-time installdotnet tool install --global Nikcio.OpenApiCodeGen
# Any spec — local file or URL, JSON or YAMLopenapi-codegen https://petstore3.swagger.io/api/v3/openapi.json -o Petstore.cs -n MyApp.Petsrecord in one passopenapi: 3.0.3
info:
title: Petstore
version: 1.0.0
components:
schemas:
Pet:
type: object
required: [name, photoUrls]
properties:
id:
type: integer
format: int64
name:
type: string
photoUrls:
type: array
items:
type: string
status:
type: string
description: pet status in the store
enum: [available, pending, sold]/// <summary>
/// pet status in the store
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PetStatus
{
[JsonStringEnumMemberName("available")]
Available,
[JsonStringEnumMemberName("pending")]
Pending,
[JsonStringEnumMemberName("sold")]
Sold
}
public record Pet
{
[JsonPropertyName("id")]
public long? Id { get; init; }
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("photoUrls")]
public required IReadOnlyList<string> PhotoUrls { get; init; }
/// <summary>
/// pet status in the store
/// </summary>
[JsonPropertyName("status")]
public PetStatus? Status { get; init; }
}Every mapping is deliberate: int64 becomes long, required stays required, descriptions become XML doc comments, and enums keep their wire names.
Records, not classes
Immutable record types with init-only properties. The spec says a property is required? You get the required keyword. Everything else is honestly nullable.
Enums that round-trip
available on the wire, PetStatus.Available in your code. Mapped with [JsonStringEnumMemberName], so your API contract never changes.
Immutable by default
Arrays become IReadOnlyList<T>, maps become IReadOnlyDictionary<K, V>. Want List<T> back? --mutable-arrays is one flag away.
Composition handled
allOf, oneOf, and anyOf become real C# types — no manual merging, no hollow models with a TODO in the summary.
Remote specs welcome
Feed it a URL or a file path. The generator fetches, parses, and emits in one pass — which makes it right at home in CI.
Zero runtime dependencies
The generated code depends on exactly one thing you already ship: System.Text.Json. No NuGet packages, no version conflicts, nothing new to vet.
The README says opinionated. Here’s what that means in practice:
with expressions, init-only — because API models are data, not behavior.T?. The compiler — not a NullReferenceException in production — tells you what can be missing.PetStatus.Available, not (PetStatus)0.