JSON to C# Class Generator

Paste a response body and get the model file back. Nested objects become their own types, every entry of an array is merged so a key missing from one record comes through nullable, and two objects with the same shape share one type instead of being written twice.

JSON to C# generation bench

Output style
JSON input.json
C# outputWaiting for JSON
0Types written
0Properties typed
0Nesting depth
0Nullable properties
  • Merges every array entry
  • Reuses matching shapes
  • Runs in your tab

Turning a response body into a C# model file

A JSON document carries six types. C# carries a type system deep enough to describe all of them, plus the distinctions JSON has no way to express: whether a number is money, whether a string is an identifier, whether an absent key means null or means nothing at all. Writing the model by hand is where those distinctions go wrong, usually on the one field somebody forgot could arrive empty. Generating from real data settles most of it in a single pass, and the three output styles above match the three reasons a .NET project wants a model class in the first place.

Class, record, or init-only

The three buttons produce genuinely different code, not three coats of paint on the same properties. Pick by how the object gets built and whether anything is allowed to change it afterwards.

StyleMutabilityMinimum C#Reach for it when
ClassFully mutable, get; set;C# 8The model is bound by ASP.NET Core, mapped by EF Core, or edited after deserialization
RecordImmutable positional parametersC# 9The payload is a message you read once, compare, and pass along without touching
Init-onlySettable at construction, frozen afterC# 11 for requiredYou want object initializer syntax and a compiler error when a field is left out

The record style writes a positional record, so Equals, GetHashCode, deconstruction and a readable ToString all arrive free. That value equality is the real reason to pick it: two ticket objects with matching fields compare equal, which is what you want when diffing two responses or caching by content. The trade is that a positional record has no parameterless constructor, so anything doing property-by-property binding rejects it.

The init-only style marks every non-nullable property required. Miss one in an object initializer and the build fails rather than the field quietly sitting at null. That needs C# 11, so on an older target framework switch to the plain class style instead.

Which serializer the attributes target

C# property names are PascalCase. JSON keys usually are not. Something has to carry that mapping, and the two libraries spell it differently.

System.Text.Json
[JsonPropertyName("minutes-to-first-reply")]public double MinutesToFirstReply { get; set; }
Newtonsoft.Json
[JsonProperty("minutes-to-first-reply")]public double MinutesToFirstReply { get; set; }

The attribute is written whenever the C# name is not a byte for byte match with the JSON key, casing included. That looks redundant on a key like subject mapping to Subject, and it is deliberate. JsonSerializer.Deserialize called directly is case sensitive by default. ASP.NET Core flips PropertyNameCaseInsensitive on in its own options, so the same model works there without attributes and breaks the moment you call the serializer yourself in a console app or a background worker. Writing the attribute makes the model behave the same in both places.

Switching the dropdown to No attributes gives you a clean file with nothing but properties. Use it when your serializer options already carry a naming policy, such as JsonNamingPolicy.CamelCase, and you would rather keep that single rule in one place than repeat it on every property.

Records need a different attribute spelling. On a positional record the parameter is not the property, so a bare attribute lands on the constructor parameter and the serializer never sees it. The output uses [property: JsonPropertyName("...")] to push the attribute onto the generated property. Copying an attribute off a class into a record without that prefix is one of the quieter ways to end up with every field null after a deserialize that threw no error.

How each JSON value becomes a C# type

Inference reads values, not keys. Every sample of a field contributes, so the type widens as more of the payload is read.

What the JSON holdsC# type writtenReasoning
"text"stringDefault for any string with no recognised format
"2026-08-14T09:21:05Z"DateTimeOffsetOnly when every sample of the field parses as a timestamp
"2026-08-14"DateOnlyDate with no time part, which needs .NET 6 or newer
"8f2b1c4e-0d3a-..."GuidCanonical 8-4-4-4-12 hex form
2intWhole numbers inside the 32 bit range
9007199254740000longPast int.MaxValue, so the whole field widens
18.5 or a mixdoubleOne decimal sample widens the field, even if the rest are whole
trueboolRead before number, since JSON keeps them separate
null alongside valuesT?Null is folded into the type rather than replacing it
null and nothing elseobject?No sample ever showed the real type
[]List<object>An empty array carries no element type
{ ... }A generated typeNamed after the key that held it, singularised inside arrays

Turning off Read dates and GUIDs keeps every string as string. Do that when your sample is small. A field where nine values parse as timestamps and the tenth is the word pending would be typed DateTimeOffset from the first nine, then throw a JsonException on the tenth in production.

Nullable reference types change what the output has to say

With <Nullable>enable</Nullable> in your project file, the compiler treats string as never null and warns on any property it cannot prove gets assigned. That is CS8618, and a generated model with twenty string properties produces twenty of them. The class style silences it the way most hand written models do.

public string Subject { get; set; } = null!;public List<string> Tags { get; set; } = new();public string? ResolvedBy { get; set; }

Read those three lines carefully, because they say three different things. Subject promises the compiler a value it does not actually have yet, which is a promise the deserializer keeps and a manually constructed object does not. Tags gets a real empty list, so iterating a payload that omitted the key gives you zero items rather than a NullReferenceException. ResolvedBy is honestly nullable, so every call site is forced to check.

Clear the checkbox and the annotations disappear on reference types, leaving plain string throughout. Value types keep their question mark either way, since int? is a different type rather than a compiler annotation.

What an array of objects tells the generator

This is the part worth understanding, and the reason to paste a whole response rather than one trimmed record. Every element of an array is folded into a single shape. A key present in all of them stays required. A key present in some of them comes through nullable.

JSON in
"replies": [{ "author": "Priya Raghunathan","body": "Certificate expired." },{ "author": "Marcus Adeyemi","body": "Reissued.","internal": true }]
C# out
public class Reply
{public string Author { get; set; } = null!;public string Body { get; set; } = null!;public bool? Internal { get; set; }}

Three decisions landed there. The array key replies was singularised, so the type is Reply and the property is List<Reply>. The internal key appeared in one entry out of two, so it came through as bool? rather than bool. And the notes panel under the editors says so, in the words "missing from 1 of 2 records", which is the signal that your sample is doing real work.

Two objects with matching field names, matching types and matching optionality get one type between them. In the sample payload, requester and assignee both hold a name, an email and a flag, so both properties are typed Requester and no Assignee class is written. On a large payload this is the difference between six types and sixty. When the two are separate concepts in your domain, copy the class after generation and point one property at each.

Key names C# will not take

JSON keys are arbitrary strings. C# identifiers are not, and four collisions turn up constantly in real payloads.

Reserved words are handled too. A key literally named class becomes the property Class, which is legal, since C# keywords are lowercase. The at sign prefix only appears in the rare case where PascalCase still lands on a keyword.

Where the guess is wrong

Type inference is a reading of the data in front of it, not a schema. Five failures are worth knowing before the file goes into a repository.

When you need a contract rather than a reading, generate a JSON Schema from the same payload and treat the schema as the source of truth. Inference from examples is a fast start, not a guarantee.

A working pass, start to finish

Assume you are wiring up an API client and want the response deserialized into typed objects.

  1. Call the endpoint and copy a full response body. Two or three of them concatenated into an array is better, since optional fields only reveal themselves across several records.
  2. Paste it above, set the root type name to match the resource, and pick the style your project uses.
  3. Read the notes panel. Anything marked as a warning is a field where the payload did not carry enough information.
  4. Download the file into your project, then fix what the data could not describe: money to decimal, status strings to an enum, any array that came through as List<object>, and any object that is really a dictionary.
  5. Deserialize with JsonSerializer.Deserialize<SupportTicket>(body) and run it against a handful of real responses before trusting it.

Step four is the one people skip. The generator gets you to roughly ninety percent of a usable model in a second. The last ten percent is domain knowledge no tool reads off a payload.

What this generator leaves out

Nothing you paste is uploaded. The parser and the generator both run inside this page, so a payload holding customer records or tokens stays on your machine. Load the page once, drop your network connection, and everything above keeps working.

Questions about generating C# from JSON

Nullability, serializer attributes, records versus classes, and the fields inference gets wrong.

Should I generate a class or a record?

It depends on what happens to the object after it is built. A class with get and set is what ASP.NET Core model binding, EF Core and most mapping libraries expect, because they construct the object empty and fill it property by property. A positional record has no parameterless constructor, so those tools reject it, but in exchange you get value equality, deconstruction and a readable ToString for free. Use a record for a message you read once and pass along. Use a class for anything that gets bound, tracked or edited.

Why is there a null forgiving operator on my string properties?

Because nullable reference types are enabled and the compiler cannot see that the deserializer will assign those properties. Without the assignment it raises CS8618 on every non-nullable reference property. The generated value tells the compiler to trust you, which is true when the object comes from a deserializer and false when you construct one by hand in a test. If that trade bothers you, switch the output to the init-only style, where the required modifier makes the compiler enforce the same thing at every construction site.

Which serializer do the generated attributes work with?

Whichever one you pick in the Attributes dropdown. System.Text.Json is the built-in choice from .NET Core 3.0 onward and uses JsonPropertyName. Newtonsoft.Json is the long-standing third party library and uses JsonProperty. The two attribute types are not interchangeable, and a model carrying the wrong one deserializes into a set of default values without raising an error, which is a slow bug to find. Choosing No attributes gives a clean file for projects that set a naming policy in serializer options instead.

My decimal values came through as double. How do I fix that?

By editing them, and you should. JSON has one number type, so a price of 18.5 and a latency of 18.5 look identical to any parser. The generator picks double because it is the safe general choice for a fractional number. Money needs decimal, which gives exact base ten arithmetic and avoids the rounding surprises that make an invoice total off by a cent. Search the generated file for double and change every field that represents currency before you use it.

Why did two of my objects share a single class?

Because their shapes matched exactly, down to property names, inferred types and which properties were nullable. Writing one type instead of two identical ones keeps the file readable, and the notes panel says which fields were merged. When the two objects are separate concepts in your domain, copy the class after generation, rename the copy, and point one property at each. The tool compares structure, which is all a payload exposes.

Can I paste a JSON array at the top level?

Yes, and it is the better way to use this tool. Every element is merged into one shape before any code is written, so a key missing from some entries comes through nullable instead of being silently assumed required. The generated file ends with a comment reminding you to deserialize into List of the root type rather than the root type on its own.

What C# version does the output need?

C# 8 for the class style, which is where nullable reference types arrived. The record style needs C# 9. The init-only style needs C# 11, because it marks non-nullable properties with the required modifier. DateOnly is separate from all of that and needs .NET 6 or newer at runtime. If you target something older, clear the date detection checkbox and those fields stay as plain strings.

How do I handle a JSON object whose keys are ids?

Replace the generated type with a Dictionary of string to your item type. A payload shaped like an id-keyed map produces a class with one property per id, which is useless the moment a new id appears. Nothing in the JSON distinguishes a map from a record with unusual key names, so this is a judgement the tool cannot make. Generate the item type from one of the values, then wire the dictionary in yourself.

Is my JSON sent to a server?

No. Parsing, type inference and code generation all run inside this page in JavaScript. There is no request to any server after the page loads, so a payload containing production data never leaves your machine. Nothing is stored between visits either, and closing the tab clears both editors.