CLI Code Generation

May 12, 2026 · View on GitHub

Overview

corvusjson is the .NET global tool that generates strongly-typed C# models from JSON Schema files. It produces the same output as the Roslyn incremental source generator, but runs ahead of time from the command line — making it suitable for CI/CD pipelines, pre-generation workflows, and scenarios where you need to inspect or version-control the generated code.

The tool supports all major JSON Schema drafts (Draft 4, 6, 7, 2019-09, and 2020-12), OpenAPI 3.0, and YAML input.

Tip: If you only need validation and annotation collection without the full type system, use --codeGenerationMode SchemaEvaluationOnly to generate a standalone schema evaluator.

Installation

dotnet tool install --global Corvus.Json.Cli

Or as a local tool:

dotnet new tool-manifest
dotnet tool install Corvus.Json.Cli

Legacy shim: The previous package Corvus.Json.CodeGenerator (command: generatejsonschematypes) still works but defaults to the V4 engine and displays a deprecation warning. New projects should use Corvus.Json.Cli.

Quick Start

Generate types from a schema file:

corvusjson jsonschema Schemas/person.json \
    --rootNamespace MyApp.Models \
    --outputPath Generated/

This reads person.json, generates one or more C# files into Generated/, and uses MyApp.Models as the root namespace.

Commands

generate (Default)

Generate C# types from a single schema file. This is the default command when using the jsonschema subcommand — you can omit the command name.

corvusjson jsonschema <schemaFile> [OPTIONS]

Required arguments:

  • <schemaFile> — Path to the JSON Schema file
  • --rootNamespace — Root namespace for generated types

Common options:

OptionDefaultDescription
--outputPathDirectory for generated .cs files
--outputRootTypeNameDerivedOverride the .NET type name for the root type. If omitted, the name is derived automatically using naming heuristics (see Naming Heuristics below).
--rootPathJSON Pointer to the root element (e.g., #/definitions/Person)
--rebaseToRootPathfalseRebase the document as if rooted at --rootPath
--useSchemaAuto-detectFallback schema draft: Draft4, Draft6, Draft7, Draft201909, Draft202012, OpenApi30
--assertFormattrueEnforce format keyword as a validation assertion
--optionalAsNullableNoneHow to handle optional properties: None or NullOrUndefined
--useImplicitOperatorStringfalseUse implicit (vs explicit) conversion to string
--disableOptionalNamingHeuristicsfalseDisable all optional naming heuristics at once (see Naming Heuristics)
--disableNamingHeuristicDisable a specific naming heuristic by name (repeatable; see Naming Heuristics)
--useUnixLineEndingsfalseUse Unix line endings (\n) instead of Windows (\r\n) in generated files
--yamlfalseEnable YAML schema support
--addExplicitUsingsfalseInclude explicit using statements for standard implicit usings
--engineV5Code generation engine: V5 (Corvus.Text.Json) or V4 (legacy Corvus.Json.ExtendedTypes)
--codeGenerationModeTypeGenerationTypeGeneration (types only), SchemaEvaluationOnly (standalone evaluator only), or Both
--outputMapFileWrite a JSON map of all generated files

Examples:

# Generate with a custom root type name
corvusjson jsonschema Schemas/person.json \
    --rootNamespace MyApp.Models \
    --outputPath Generated/ \
    --outputRootTypeName Person

# Generate from a specific definition within a schema
corvusjson jsonschema Schemas/api.json \
    --rootNamespace MyApp.Models \
    --outputPath Generated/ \
    --rootPath "#/definitions/Address" \
    --rebaseToRootPath

# Treat optional properties as nullable
corvusjson jsonschema Schemas/config.json \
    --rootNamespace MyApp.Config \
    --outputPath Generated/ \
    --optionalAsNullable NullOrUndefined

config

Generate types from a configuration file that specifies multiple schemas and shared settings:

corvusjson config myconfig.json [--engine V5]

Example configuration file

{
    "rootNamespace": "MyApp.Models",
    "outputPath": "Generated/",
    "useSchema": "Draft202012",
    "assertFormat": true,
    "optionalAsNullable": "None",
    "typesToGenerate": [
        {
            "schemaFile": "Schemas/person.json",
            "outputRootTypeName": "Person"
        },
        {
            "schemaFile": "Schemas/order.json",
            "outputRootTypeName": "Order",
            "outputRootNamespace": "MyApp.Models.Orders"
        }
    ],
    "additionalFiles": [
        {
            "canonicalUri": "https://example.com/schemas/address.json",
            "contentPath": "Schemas/address.json"
        }
    ],
    "namedTypes": [
        {
            "reference": "https://example.com/schemas/person.json#/$defs/Name",
            "dotnetTypeName": "PersonName",
            "dotnetNamespace": "MyApp.Models"
        }
    ],
    "namespaces": {
        "https://example.com/schemas/": "MyApp.ExternalModels"
    }
}

The config command is ideal when you have multiple schemas to generate from, shared $ref dependencies, or you want to explicitly name generated types.

Root-level properties

PropertyRequiredDefaultDescription
rootNamespaceYesThe default .NET namespace for all generated types. Individual schemas can override this with outputRootNamespace.
typesToGenerateYesArray of schemas to process. Each entry specifies a schema file and optional overrides (see below).
outputPathNoDirectory where generated .cs files are written.
outputMapFileNoPath for a JSON manifest listing every generated file, useful for build systems that need to track outputs.
useSchemaNoDraft202012Fallback schema draft when vocabulary analysis cannot determine the draft from the $schema keyword. Values: Draft4, Draft6, Draft7, Draft201909, Draft202012, OpenApi30.
assertFormatNotrueWhen true, the format keyword is enforced as a validation assertion. When false, format is treated as an annotation only (per the JSON Schema specification).
optionalAsNullableNoNoneControls how optional properties are represented in generated types. None: optional properties use the same type as required properties — you check for Undefined explicitly. NullOrUndefined: optional properties generate as .NET nullable types (T?), and both JSON null and missing properties map to C# null.
additionalFilesNoPre-load external schema files so $ref references can resolve without network access (see below).
namedTypesNoExplicitly assign .NET names to specific schema definitions that would otherwise get auto-generated names (see below).
namespacesNoA dictionary mapping schema base URIs to .NET namespaces. Any schema whose canonical URI starts with a given key is generated into the corresponding namespace. For example, "https://example.com/schemas/": "MyApp.External" places all schemas under that URI prefix into MyApp.External.
disableOptionalNameHeuristicsNofalseWhen true, disables all optional naming heuristics at once. Cannot be combined with disabledNamingHeuristics.
disabledNamingHeuristicsNoAn array of specific naming heuristic names to disable. Use corvusjson listNameHeuristics to see available names. Cannot be combined with disableOptionalNameHeuristics.
useImplicitOperatorStringNofalseWhen true, conversion operators to string are implicit rather than explicit. Use with care — implicit conversions can cause unintended string allocations.
useUnixLineEndingsNofalseWhen true, generated files use Unix line endings (\n) instead of Windows (\r\n).
supportYamlNofalseWhen true, enables YAML support. Schema and document files can be YAML, JSON, or a mixture.
addExplicitUsingsNofalseWhen true, generated files include using statements for standard implicit usings. Enable this if your project does not use implicit usings.

typesToGenerate entries

Each entry in typesToGenerate specifies one schema to process:

PropertyRequiredDefaultDescription
schemaFileYesPath to the JSON Schema file to process, relative to the config file.
outputRootTypeNameNoDerivedThe .NET type name for the root type generated from this schema. If omitted, the name is derived automatically using the standard naming heuristics — typically from the schema's title keyword, $id URI fragment, or the schema file name (see Naming Heuristics).
outputRootNamespaceNorootNamespaceOverride the .NET namespace for this schema's root type. Useful when different schemas should go into different namespaces.
rootPathNoA JSON Pointer within the schema document to use as the root type. For example, #/$defs/Address generates types starting from the Address definition rather than the document root.
rebaseToRootPathNofalseWhen true (and rootPath is set), the document is rebased as if the element at rootPath were the document root. This affects how $ref references within the extracted subtree are resolved.

additionalFiles entries

Pre-load schema files that other schemas reference via $ref. This avoids network calls during generation and ensures deterministic, reproducible builds — particularly important in CI/CD environments:

PropertyRequiredDescription
canonicalUriYesThe canonical URI that other schemas use in their $ref to reference this file. Must match the URI used in $ref exactly.
contentPathYesLocal file path to the schema, relative to the config file.

For example, if person.json contains "$ref": "https://example.com/schemas/address.json", add an entry with that URI pointing to your local copy of the address schema.

namedTypes entries

By default, the generator derives .NET type names from the schema structure. Use namedTypes to override specific types with explicit names — useful when the auto-generated name is awkward or when you need a type to live in a specific namespace:

PropertyRequiredDescription
referenceYesThe fully-qualified schema reference (base URI + JSON Pointer fragment), e.g. https://example.com/schemas/person.json#/$defs/Name.
dotnetTypeNameYesThe .NET type name to use, e.g. PersonName.
dotnetNamespaceNoWhen set, the type is generated directly in this namespace rather than as a nested type of its containing schema type.

validateDocument

Validate a JSON or YAML document against a schema:

corvusjson validateDocument Schemas/person.json data.json

Output includes pass/fail status for each validation rule, with file path, line, column, and the schema evaluation location for failures.

listNameHeuristics

List all available naming heuristics. Some are optional and can be disabled with --disableNamingHeuristic:

corvusjson listNameHeuristics

version

Display the tool version and build information:

corvusjson version

Source Generator vs. CLI Tool

Both produce identical output — the same readonly struct types with the same property accessors, validation, serialization, and implicit conversions. The difference is when and how they run:

AspectSource GeneratorCLI Tool
WhenAt build time, automaticallyOn demand, from the command line
Triggered by[JsonSchemaTypeGenerator] attributeExplicit corvusjson jsonschema invocation
Output locationobj/ (not checked in)Any directory (can be checked in)
IDE integrationFull IntelliSense as you typeIntelliSense after generation + build
Best forMost projectsCI pipelines, inspecting output, multi-schema configs

When to Use the CLI Tool

  • You want to inspect or version-control the generated code
  • You have a multi-schema configuration file with shared settings
  • You need to pre-generate types in a CI/CD pipeline before the build
  • You want to validate documents against schemas from the command line
  • You are migrating from V4 and want to compare V4 vs V5 output

When to Use the Source Generator

  • You want zero-configuration code generation at build time
  • You prefer generated code to stay out of source control
  • You want instant IntelliSense as you edit schema files

Generated Output

The tool generates readonly struct types that are thin wrappers over pooled JSON data. For a schema like:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "required": ["name"],
    "properties": {
        "name": { "type": "string", "minLength": 1 },
        "age": { "type": "integer", "format": "int32", "minimum": 0 }
    }
}

The tool generates:

  • Type-safe property accessors: person.Name returns a strongly-typed string value
  • Validation: person.EvaluateSchema() validates against the full schema
  • Implicit conversions: (string)person.Name extracts the .NET value
  • Mutable builder: person.CreateBuilder(workspace) creates a mutable copy
  • Pattern matching: Match() methods for oneOf/anyOf discriminated unions
  • Serialization: WriteTo(Utf8JsonWriter) for zero-allocation output

Schema Draft Support

Draft$schema URI--useSchema Value
Draft 4http://json-schema.org/draft-04/schemaDraft4
Draft 6http://json-schema.org/draft-06/schemaDraft6
Draft 7http://json-schema.org/draft-07/schemaDraft7
Draft 2019-09https://json-schema.org/draft/2019-09/schemaDraft201909
Draft 2020-12https://json-schema.org/draft/2020-12/schemaDraft202012
OpenAPI 3.0(custom vocabulary)OpenApi30

The tool auto-detects the schema draft from the $schema keyword. Use --useSchema only as a fallback when the keyword is missing.

Output Map File

Use --outputMapFile to generate a JSON manifest of all generated files:

corvusjson jsonschema Schemas/person.json \
    --rootNamespace MyApp.Models \
    --outputPath Generated/ \
    --outputMapFile generated.map.json

This is useful for build systems that need to track which files were generated.

Naming Heuristics

When --outputRootTypeName is not specified, the tool applies a chain of naming heuristics to derive an idiomatic C# type name from the schema. Heuristics are tried in priority order (lowest number first) and the first match wins. The same heuristics are also applied to nested types, sub-schemas, and property types throughout the generated code.

How the root type name is determined

The tool tries these sources in order:

  1. Explicit name (--outputRootTypeName or outputRootTypeName in config) — used as-is, no heuristics applied
  2. Custom keywords (priority 100) — the title keyword or the last segment of the $id URI
  3. Schema reference (priority 1000) — the last segment of the JSON Pointer fragment (e.g., #/$defs/AddressAddress) or the schema file name without extension (e.g., person.jsonPerson)
  4. Documentation (priority 1500, optional) — the schema description if it is short (under 64 characters) and unique within its parent
  5. Fallback — a name derived from the file path

For example, given this schema in Schemas/person.json:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "title": "Person",
    "type": "object",
    "properties": {
        "name": { "type": "string" }
    }
}

The root type name is Person (from the title keyword). If title were absent, it would be Person (from the file name person.json).

Full heuristic list

Heuristics marked optional can be disabled with --disableOptionalNamingHeuristics (disables all optional) or --disableNamingHeuristic <name> (disables one at a time). Non-optional heuristics are always active.

PriorityHeuristicOptionalWhat it does
1WellKnownTypeNameHeuristicNoMaps well-known schema patterns to built-in types (e.g., the "any" schema maps to JsonElement).
2SimpleCoreTypeNameHeuristicNoMaps simple schemas with a single core type and optional format to global types: JsonString, JsonInt32, JsonUuid, JsonBoolean, etc. These types are shared — no per-schema code is generated.
2BuiltIn*TypeNameHeuristicNoA family of heuristics (BuiltInStringTypeNameHeuristic, BuiltInNumberTypeNameHeuristic, etc.) that map bare "type": "string", "type": "number", "type": "integer", "type": "boolean", "type": "null", "type": "object", and "type": "array" schemas to their respective global types.
100CustomKeywordNameHeuristicNoExtracts the name from the title keyword or the last segment of the $id URI. This is the most common source for root type names.
1000BaseSchemaNameHeuristicNoFor root or $defs-level schemas: extracts the name from the JSON reference fragment (e.g., #/$defs/AddressAddress) or the schema file name (e.g., address.jsonAddress).
1500DocumentationNameHeuristicYesUses the schema description as the type name if it is short enough (under 64 characters) and unique within its parent.
1500DefaultValueNameHeuristicYesFor schemas with only a default keyword: generates names like DefaultValueTrue or DefaultValueActive.
1550RequiredPropertyNameHeuristicYesFor nested schemas: uses Required + property names, e.g., a schema requiring id and name becomes RequiredIdAndName.
1600ConstPropertyNameHeuristicYesFor nested schemas with const properties: uses With + const values, e.g., WithStatusActive.
1600SingleTypeArrayNameHeuristicYesFor array schemas: appends Array to the item type name, e.g., an array of Item becomes ItemArray.
10000SubschemaNameHeuristicNoFor inline nested schemas: extracts the name from the JSON reference path segment.
11000PathNameHeuristicNoFinal fallback for nested schemas: extracts the name from the last path segment of the JSON reference.

Collision resolution

When a derived name collides with its parent type or a sibling, the tool automatically appends a suffix based on the schema kind (Object, Array, String, etc.). For example, if both a parent and child would be named User, the child becomes UserObject.

Disabling heuristics

# Disable a specific heuristic by name
corvusjson jsonschema Schemas/person.json \
    --rootNamespace MyApp.Models \
    --outputPath Generated/ \
    --disableNamingHeuristic DocumentationNameHeuristic

# Disable all optional heuristics at once
corvusjson jsonschema Schemas/person.json \
    --rootNamespace MyApp.Models \
    --outputPath Generated/ \
    --disableOptionalNamingHeuristics

Run corvusjson listNameHeuristics to see all available heuristics and which ones are optional.