Skip to content

OpenAPI Code Generator

Your OpenAPI spec is already a contract. Stop hand-writing C# for it — generate records, string enums, and immutable collections that match it exactly.
Terminal window
# One-time install
dotnet tool install --global Nikcio.OpenApiCodeGen
# Any spec — local file or URL, JSON or YAML
openapi-codegen https://petstore3.swagger.io/api/v3/openapi.json -o Petstore.cs -n MyApp.Pets
petstore.yamlyour spec
openapi: 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]
Petstore.csgenerated
/// <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:

  • Records over classes. Value semantics, with expressions, init-only — because API models are data, not behavior.
  • Immutable over mutable. Collections are read-only unless you say otherwise. If your code mutates a DTO, that deserves a second look.
  • Nullable over hopeful. If the spec doesn’t require it, it’s T?. The compiler — not a NullReferenceException in production — tells you what can be missing.
  • String enums over magic numbers. PetStatus.Available, not (PetStatus)0.
  • Models over megazords. You get types, not a generated HTTP client you didn’t ask for. Bring your own client, or none.