One paste, twenty properties, zero typos

The vendor's API documentation includes a sample response — 140 lines of JSON with customer objects, order arrays, and a billing address nested three levels deep. Your C# client needs a class for every shape in there, by hand, tonight. Twenty minutes of typing PascalCase properties against camelCase keys, and the typo you won't catch until runtime is already in there: CusterName, bound to nothing, silently null.

This is the job every .NET developer knows and no .NET developer loves: translating JSON into C# types. It's mechanical work that rewards exactness and punishes confidence, which is precisely the profile of work that should never be done by hand. A converter does the translation in one pass, and the interesting engineering — what the client does with the typed data — starts immediately after.

The JSON to C# Class Converter generates modern C# from any payload you paste: PascalCase properties, System.Text.Json attributes preserving the original keys, nullable annotations where your sample shows null, decimal for money-looking numbers, and nested classes for nested shapes. For the rest of the type-generation bench, the JSON Schema Generator builds validation schemas, the JSON to TypeScript Interface tool covers the frontend, and the JSON Formatter & Validator cleans up whatever you paste before conversion.

Below: the workflow, the type-inference rules, a full payload converted end to end, the corners where generators get confused, and the classes-versus-records-versus-dynamic decision that every generated file quietly embeds.

Converting your first payload

A realistic customer payload carries the whole feature set — nesting, nulls, money, dates, and arrays. Here's the walkthrough from paste to compiling code.

  1. Paste the JSON sample

    Any valid JSON works — a single object, an array of objects, a wrapper envelope with data and pagination fields. The converter parses it locally and reports the shapes it found.

  2. Review the inferred types

    4021 becomes int, 2500.00 becomes decimal, the ISO timestamp becomes DateTime, the null email becomes string?, and the orders array becomes List<Order> with Order generated from its first element.

  3. Check the naming and attributes

    customerName becomes CustomerName with [JsonPropertyName("customerName")] attached — PascalCase in C#, camelCase on the wire, no manual mapping to maintain.

  4. Switch output mode if you want records

    Classes are the default. The record mode emits init-only properties for immutable DTOs, which suits most API response types.

  5. Rename where the API was lazy

    Root classes can be renamed from the default — a wrapper called RootResponse becomes CustomerApiResponse with two keystrokes, and nested classes follow the property they came from.

  6. Copy into your project

    The output compiles as-is under <Nullable>enable</Nullable>, with System.Text.Json as the only dependency — the serializer Microsoft ships in the box with modern .NET.

Total elapsed time from paste to compiled: under a minute, most of which is you reading code you'd otherwise have typed. The typo class of bug — the CusterName problem — exits the codebase entirely.

How JSON shapes become C# types

Every JSON value maps onto a small set of C# types, and the converter's inference rules are visible rather than magical. Here is the table it applies, field by field.

JSON shape in your sampleGenerated C# typeThe reasoning
Integer that fitsint2,147,483,647 ceiling; anything beyond promotes
Integer too large for intlong64-bit IDs are the norm in distributed systems
Number with decimal placesdecimalExact base-10 for money; switch to double for measurements
true / falseboolNo drama
String that parses as ISO 8601DateTime (or DateTimeOffset)Typed dates beat re-parsing strings forever
String that parses as a GUIDGuidStructured identifiers deserve structured types
null anywhere in the samplenullable annotationstring? for strings, int? for numbers — the sample is telling you something
Array of objectsList<GeneratedClass>One class per distinct object shape in the array
Array of primitivesList<string>, List<int>, ...Element type drives the generic
Object with dynamic keysDictionary<string, TValue>When keys are data, not schema
The inference table the converter applies. Every rule has an override — the sample is evidence, not law.

The naming rule is mechanical: JSON keys become PascalCase properties — customer_name, customerName, and customer-name all produce CustomerName — while the JsonPropertyName attribute preserves the original spelling for serialization. That last part matters more than it looks. C# conventions and API conventions pull in different directions, and the attribute is the permanent treaty between them: rename the C# property freely, and the wire format never moves.

Nullability deserves its own word because it changes how the generated code behaves under modern compilers. When your sample contains "email": null, the converter emits string? Email. Under nullable reference types — the default in every recent .NET project template — the compiler now warns you wherever you dereference Email without a check. The sample's null was a fact about the API's behavior; the annotation turns that fact into a machine-checked contract.

A real payload, converted

Input and output, complete. This is the customer object from the walkthrough, run through the converter with default settings — classes, nullable annotations on, decimal for money.

Input JSON:

{
  "id": 4021,
  "customerName": "Dana Whitmore",
  "email": null,
  "vip": true,
  "creditLimit": 2500.00,
  "joinedAt": "2024-03-18T09:24:00Z",
  "orders": [
    {
      "orderId": "A-118",
      "total": 84.50,
      "items": 3,
      "shipped": true
    }
  ],
  "tags": ["wholesale", "net30"]
}
Generated C#:

public class Customer
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("customerName")]
    public string CustomerName { get; set; }

    [JsonPropertyName("email")]
    public string? Email { get; set; }

    [JsonPropertyName("vip")]
    public bool Vip { get; set; }

    [JsonPropertyName("creditLimit")]
    public decimal CreditLimit { get; set; }

    [JsonPropertyName("joinedAt")]
    public DateTime JoinedAt { get; set; }

    [JsonPropertyName("orders")]
    public List<Order> Orders { get; set; }

    [JsonPropertyName("tags")]
    public List<string> Tags { get; set; }
}

public class Order
{
    [JsonPropertyName("orderId")]
    public string OrderId { get; set; }

    [JsonPropertyName("total")]
    public decimal Total { get; set; }

    [JsonPropertyName("items")]
    public int Items { get; set; }

    [JsonPropertyName("shipped")]
    public bool Shipped { get; set; }
}
Deserializing with the generated classes

Input

var customer = JsonSerializer.Deserialize<Customer>(json, options);

Output

customer.Id == 4021, customer.Email == null (compiler-warned if you forget to check), customer.Orders[0].Total == 84.50m exactly — decimal, not 84.49999999. One line of your code, two generated classes, zero hand-typed properties.

Count what the tool did: eight properties and a nested class from one paste, nullability derived from evidence, money typed as money, and every original key preserved in attributes. Typing this by hand is eleven minutes; reviewing generated output is forty seconds. The ratio holds whether the payload has two fields or two hundred.

Type decisions: decimal, long, and nullable

Three type decisions carry most of the real-world weight in generated code, and each one is a story about a bug the type system can prevent.

decimal versus double is the money question. JSON numbers are text on the wire, but 84.50 parsed as a double becomes the nearest binary approximation — close enough to print correctly most of the time and wrong in exactly the ways finance software cannot tolerate. Decimal keeps 84.50 as the exact base-10 value it says it is. The converter defaults money-shaped numbers to decimal; when a field is actually a measurement — latitude, a probability, a weight — flip it to double in the output before copying, because decimal's exactness costs range and speed that scientific values don't need.

long versus int is the identifier question. Int tops out at 2,147,483,647 — a number that sounded astronomical in 1995 and that Twitter's Snowflake IDs passed a decade ago. Generated code that reads an API's id field as int compiles cleanly and then throws OverflowException, or worse, silently truncates, the first time a real ID crosses the line. The converter promotes to long whenever the sample shows big values, and the manual override exists for the subtler case: small IDs today, distributed-systems IDs tomorrow.

Nullable annotations are the contract question. A JSON field that's null in your sample is the API telling you, in the only language it has, that this field can be absent. Encoding that as string? turns tribal knowledge into compiler enforcement: dereference without checking and the build warns. Teams that adopt generated nullables report the same arc — a week of annoying warnings, then a permanent drop in NullReferenceException at the API boundary, where most of them used to live.

When generators get confused

JSON is a data format with no schema, which means samples can lie in ways types can't express. The known ambushes, and what to do about each.

  • Inconsistent types across records. One order carries total: 84.50, the next carries total: "84.50" as a string — sloppy APIs do ship this. The generator types from what it sees and will pick one. Fix upstream if you can; if you can't, convert to string and parse defensively in a property wrapper.
  • Arrays with different object shapes. A heterogeneous array generates one class from the first element and drops the rest. Inspect the output for missing fields — the fix is usually a union class with nullable extras, or better, an API complaint.
  • Duplicate keys in the sample. Strict parsers reject them; lenient ones take the last value. The converter flags them rather than guessing, because duplicate keys almost always mean a copy-paste error in the documentation you pasted from.
  • Dynamic keys as data. A response keyed by user ID — { "4021": {...} } — isn't a class, it's a Dictionary<string, T>. The converter detects the pattern, and manual review confirms it's data-as-keys rather than a fixed schema.
  • Empty arrays and nulls where objects should be. An empty orders array tells the generator nothing about the Order shape — there's no element to learn from. Feed a richer sample or write that class by hand; the converter can't infer structure from absence.
  • Huge payloads. A 5 MB paste works fine in the browser, but pasting a 5 MB sample from production also means you're handling production data — see the privacy section, and prefer synthetic samples that exercise every field.

Naming policies and the serializer side

The JsonPropertyName attributes are the safe default, but they're not the only configuration that works — and knowing the alternatives makes the generated code easier to live with.

System.Text.Json accepts a naming policy in JsonSerializerOptions: JsonNamingPolicy.CamelCase converts PascalCase properties to camelCase on the wire automatically, which means an API that is uniformly camelCase can drop every attribute and serialize clean. The temptation is real, and the catch is uniformity — the moment one endpoint ships snake_case or a legacy field name, the policy that removed ten attributes adds one bug that no compiler catches. Attributes are per-field treaties; policies are blanket ones. Blankets are lighter and colder.

  1. Uniform camelCase API? Try the naming policy and delete the attributes — then re-test every endpoint, because policies fail silently on mismatch.
  2. Mixed conventions, or one legacy field? Keep the generated attributes; per-field mappings degrade gracefully.
  3. snake_case APIs work the same way through naming policies in recent .NET versions; the same uniformity test applies.
  4. Renaming a property later? With attributes, only the C# side moves; with policies, the wire name moves too — which is a breaking change wearing a refactor's clothes.

The converter emits attributes by default because they make the generated code correct under every configuration, including none. Teams that standardize on a policy can strip them in one pass and enjoy leaner files; teams that don't standardize get correctness out of the box. Either way, the decision is now explicit and revisitable rather than implicit and load-bearing.

The .NET developers who live here

API client authors are the core constituency — the developer integrating against a third-party service, pasting the vendor's sample response, and needing the DTO layer in the next five minutes. Integration work is mostly this, repeated for every endpoint, and the paste-convert-copy loop compresses each repetition to under a minute.

Beyond integrations: backend developers consuming messages from queues where the payload schema lives only in the producer's code; test writers building typed fixtures from recorded responses; and teams migrating legacy Newtonsoft.Json codebases toward System.Text.Json, where the attribute difference — JsonProperty versus JsonPropertyName — is exactly the kind of detail a converter emits correctly by default while manual migration gets it backwards in three files out of ten. The longtail is anyone who has ever opened a JSON file, looked at 140 lines of structure, and felt the evening shift start.

Classes, records, or dynamic

The converter's output mode is a real architectural choice, and the three options have genuinely different consequences.

OptionBest forWatch out for
Classes (mutable)DTOs you'll populate and patch; serializer compatibility across stacksMutation spreads unless discipline follows
Records (init-only)Immutable API responses, value-equality comparisons, with-expressionsLess familiar on legacy teams; older serializers needed patches
dynamic / JsonDocumentOne-off scripts, exploratory parsing, schemas you genuinely can't knowNo compile-time checking; typos surface as runtime misses
The three postures toward JSON in C#. The converter generates the first two; the third is a lifestyle choice the compiler will warn you about.

The argument for records on modern codebases is strong: API responses are facts that arrived, not containers to be edited, and init-only properties encode that. The argument for classes is compatibility and habit — half the world's serialization infrastructure predates records and some of it still trips on them. The argument for dynamic is that you're writing a script that runs once, and setting up types would outlive the script. Choose by project, not by fashion, and let the converter's toggle make switching cheap enough to revisit when the project's answer changes.

Payloads never touch a server

The converter runs its parser, type inference, and code generator entirely in your browser. The JSON you paste is text in a textarea that gets read by local JavaScript — no upload, no history, no account. The moment you navigate away, the paste ceases to exist anywhere but your clipboard.

For a developer tool this isn't paranoia; it's due diligence. The payloads people convert are frequently production-shaped — customer records, order histories, internal API envelopes — and shipping them to a random converter website is precisely the category of accident that security reviews write up. A browser-local converter is safe to paste into from a locked-down corporate machine, from a client network under NDA, and from the hotel Wi-Fi of a conference where the payload in your buffer is someone's launch data. The tool can't exfiltrate what it never receives.

Converter questions from the daily grind

How do I convert JSON to a C# class?

Paste your JSON sample into the converter and it walks the structure, emitting one class per object shape: PascalCase properties for every field, [JsonPropertyName("originalName")] attributes so System.Text.Json still binds the original keys, and nested classes for nested objects. Copy the output into your project, and a single JsonSerializer.Deserialize call turns the payload into typed objects.

What does JsonPropertyName do?

It's the System.Text.Json attribute that maps a C# property to its JSON key when the two names differ. C# convention wants PascalCase — CustomerName — while most APIs ship camelCase or snake_case — customerName or customer_name. The attribute keeps both worlds honest: your code stays idiomatic, and deserialization still finds the right field.

Should generated properties use decimal or double?

Use decimal for money and anything where exact base-10 representation matters — 84.50 must stay 84.50, not drift into binary approximation. Use double for measurements and scientific values where floating-point scale is expected. The converter defaults to decimal when a number carries two trailing decimal digits that suggest currency, and you can override any field's type before copying.

What are nullable reference types in the generated code?

With nullable reference types enabled, string? declares that a property may legitimately be null, and the compiler enforces null-checks wherever you use it. The converter marks fields null in your sample as nullable — a JSON field that's null in the example is null in real payloads often enough to deserve the annotation. On projects without NRT enabled, the same code compiles fine and the annotations are simply ignored.

Can I generate C# records instead of classes?

Yes — the output mode switch emits positional records or records with init-only properties. Records give you value equality, concise construction, and non-destructive mutation via with-expressions, which suits API DTOs beautifully. Plain classes remain the safer default when you expect mutation, inheritance, or serializers that predate record support.

Why does the converter emit long instead of int for some numbers?

Because integers overflow silently. An int tops out at 2,147,483,647, and real-world IDs blow past it routinely — Snowflake-style identifiers from distributed systems reach 64-bit territory. The converter reads the actual values in your sample: anything that fits an int becomes int, anything larger or likely to grow becomes long. An ID that happens to be small in the sample can be pinned to long manually.

Does the converter handle nested objects and arrays?

Nested objects become nested classes — an orders array becomes List<Order>, where Order is generated from the array's first element. Arrays of primitives become List<string>, List<int>, and so on. Deeply nested payloads generate one class per level, named after the property that contains them, and you can flatten or rename the hierarchy before copying.

Is my JSON uploaded to a server?

No. Parsing, type inference, and code generation all run in your browser against the pasted text. API payloads often contain data you're not authorized to ship to a third-party website, and a converter with no upload step can't leak what it never receives. Close the tab and the paste is gone.

Also in the developer toolkit

The type-generation bench, one page each: the JSON Schema Generator when you need validation contracts rather than classes, the JSON to TypeScript Interface tool for the frontend twin of this output, the JSON to Java Class converter for the other enterprise language, and the JSON Formatter & Validator for the paste-before-the-paste. Paste-first workflows run through all of them, and none of them uploads anything.

Microsoft docs worth keeping open

Sources and further reading

We cite primary sources (standards bodies, government agencies, official documentation) wherever they exist so you can verify every number on this page yourself.