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.
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.
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.
Check the naming and attributes
customerName becomes CustomerName with [JsonPropertyName("customerName")] attached — PascalCase in C#, camelCase on the wire, no manual mapping to maintain.
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.
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.
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 sample | Generated C# type | The reasoning |
|---|---|---|
| Integer that fits | int | 2,147,483,647 ceiling; anything beyond promotes |
| Integer too large for int | long | 64-bit IDs are the norm in distributed systems |
| Number with decimal places | decimal | Exact base-10 for money; switch to double for measurements |
| true / false | bool | No drama |
| String that parses as ISO 8601 | DateTime (or DateTimeOffset) | Typed dates beat re-parsing strings forever |
| String that parses as a GUID | Guid | Structured identifiers deserve structured types |
| null anywhere in the sample | nullable annotation | string? for strings, int? for numbers — the sample is telling you something |
| Array of objects | List<GeneratedClass> | One class per distinct object shape in the array |
| Array of primitives | List<string>, List<int>, ... | Element type drives the generic |
| Object with dynamic keys | Dictionary<string, TValue> | When keys are data, not schema |
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; }
}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.
- Uniform camelCase API? Try the naming policy and delete the attributes — then re-test every endpoint, because policies fail silently on mismatch.
- Mixed conventions, or one legacy field? Keep the generated attributes; per-field mappings degrade gracefully.
- snake_case APIs work the same way through naming policies in recent .NET versions; the same uniformity test applies.
- 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.
| Option | Best for | Watch out for |
|---|---|---|
| Classes (mutable) | DTOs you'll populate and patch; serializer compatibility across stacks | Mutation spreads unless discipline follows |
| Records (init-only) | Immutable API responses, value-equality comparisons, with-expressions | Less familiar on legacy teams; older serializers needed patches |
| dynamic / JsonDocument | One-off scripts, exploratory parsing, schemas you genuinely can't know | No compile-time checking; typos surface as runtime misses |
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
- Microsoft Learn — System.Text.Json API reference(learn.microsoft.com)
- Microsoft Learn — System.Text.Json serialization overview(learn.microsoft.com)
- Microsoft Learn — nullable reference types (C# guide)(learn.microsoft.com)
- JSON Schema — the vocabulary for JSON validation and typing(json-schema.org)
We cite primary sources (standards bodies, government agencies, official documentation) wherever they exist so you can verify every number on this page yourself.