Generating JSON to C# Classes the Fast Way: Type Inference, Nullables, and JsonProperty Attributes
A practical guide to turning a JSON sample into clean C# classes — PascalCase properties, int vs long vs double inference, nullable handling, and JSON attributes.
Generating JSON to C# Classes the Fast Way
Every .NET developer hits the same wall the first time they integrate a third-party API: you have a JSON response, and you need a typed object to deserialize it into. You can walk the payload by hand with JsonElement, or you can hand-write a class with thirty properties and hope you spelled every field right. Both are tedious, and both go stale the moment the API adds a field.
The faster path is to let a sample of the JSON drive the class for you. Paste a real response, get back a .cs file you can drop into your project, and move on to the actual work. This post walks through what good generation actually does — type inference, nullable handling, PascalCase mapping, and the [JsonPropertyName] / [JsonProperty] attributes that make deserialization round-trip — using the JSON to C# Class tool as the worked example.
How a JSON shape maps to C#
The mental model is simple, and it pays to hold it explicitly:
- Each JSON key becomes one typed property.
"name": "octocat"becomespublic string Name { get; set; }. - Each nested object becomes its own class.
{ "plan": { "name": "pro" } }doesn't flatten into the parent; it lifts out into a separatePlanclass with its own properties, and the parent holds aPlanreference. - Each array becomes a
List<T>. An array of strings becomesList<string>; an array of objects becomesList<SomeClass>, where the element class is generated once and shared.
That last point matters more than it looks. When an array holds objects with slightly different shapes — [{"a":1},{"a":1,"b":2}] — a good generator folds them into a single class carrying both A and B, not two near-duplicate types. Keys that are present in some elements and absent in others become nullable, which is exactly the signal you want for optional fields.
Type inference: int, long, double, and DateTime
JSON has exactly one number type, so the generator has to infer the C# type from the value it sees. The rules that hold up in practice:
- An integer that fits in
Int32(−2,147,483,648 to 2,147,483,647) infersint. - A larger integer — millisecond timestamps, snowflake IDs, byte counts — infers
long. - A number with a fractional part infers
double. - A field that is whole in one sample and fractional in another widens to
double, the safe superset, because C# won't implicitly narrow on deserialize.
String fields get a useful upgrade too: with date detection on, an ISO-8601 string like "2026-06-13T09:41:00Z" infers DateTime instead of string. One trap to remember — a numeric-looking ID that arrives as a JSON string ("cus_4Qp9", most UUID APIs) stays string, and that is correct. long only applies to integer JSON values, never quoted ones.
PascalCase properties and the attribute that makes binding work
C# convention is PascalCase properties, but wire formats are rarely PascalCase. The generator maps site_admin to SiteAdmin, is-active to IsActive, and userId to UserId. Whenever the C# name differs from the original key, it attaches a serializer attribute so deserialization still finds the field:
[JsonPropertyName("site_admin")]forSystem.Text.Json[JsonProperty("site_admin")]for Newtonsoft (Json.NET)
This is not cosmetic. System.Text.Json matches property names case-sensitively by default, so without the attribute, SiteAdmin would silently come back as false no matter what the JSON said. The attribute is emitted only when the names actually differ — name stays Name and round-trips fine without one — which keeps the output minimal but correct. Pick the attribute style that matches your project: System.Text.Json for .NET Core 3.0+ and ASP.NET Core minimal APIs, Newtonsoft if you already depend on Json.NET.
A worked example
Here's a small GitHub-style user payload:
{
"id": 583231,
"login": "octocat",
"site_admin": false,
"created_at": "2011-01-25T18:44:36Z",
"plan": { "name": "pro", "private_repos": 999999999999 }
}
Set the root name to GitHubUser, keep System.Text.Json, and turn on date detection. The output is:
using System;
using System.Text.Json.Serialization;
public class GitHubUser
{
public int Id { get; set; }
public string Login { get; set; }
[JsonPropertyName("site_admin")]
public bool SiteAdmin { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
public GitHubUserPlan Plan { get; set; }
}
public class GitHubUserPlan
{
public string Name { get; set; }
[JsonPropertyName("private_repos")]
public long PrivateRepos { get; set; }
}
Notice the details: id (583,231) fits Int32 so it's int, while private_repos (999,999,999,999) overflows Int32 and becomes long. The nested plan object lifted out into its own GitHubUserPlan class. site_admin and created_at both got attributes because their PascalCase names differ from the wire keys; Login and Name did not. With that class in Models/GitHubUser.cs, JsonSerializer.Deserialize<GitHubUser>(body) hands you a fully typed object instead of a JsonElement you walk by hand.
Nullable handling
Two signals make a field nullable. The field appears as null somewhere in your JSON, or it's present in some array elements and missing in others — one webhook payload carries cancelled_at, the next omits it. With nullable on, those fields emit as T?: int?, DateTime?, string?. This is the cleanest way to model optional API fields, because in your handler if (e.CancelledAt is not null) distinguishes "absent" from "present" without you guessing whether default(DateTime) means the field was missing.
One edge case worth internalizing: a field that is only ever null carries no type information, so it infers as object rather than a guessed type — and object is never decorated with ?, since a reference type already holds null. If you know the real shape, feed a non-null sample so the inference has something to work with.
I learned to reach for this on a contract job where the upstream team shipped an undocumented payments webhook. I logged a dozen real payloads, pasted them as a JSON array, and the optional-field detection immediately flagged three fields I would have marked as required by eyeballing a single example — including a refunded_at that only appeared once in twelve events. Catching that as DateTime? up front saved me from a NullReferenceException that would otherwise have surfaced in production a week later.
A few habits that keep it clean
A single object has no missing-key signal, so every field reads as required unless its value is literally null. If you want to explore optionality from one sample, wrap it as [{...}] and the array path takes over. And keep the input strict — the parser uses the browser's built-in JSON.parse, so comments, trailing commas, and unquoted keys all error out with a line and column. If your sample came from a config file or a log, run it through a JSON formatter first to normalize it.
If C# isn't your only target, the same paste-a-sample approach generates TypeScript interfaces and Go structs too, which is handy when a single API contract has to be modeled on both the backend and the frontend.
Generating classes from a sample isn't a replacement for reading the API docs — it's a way to skip the mechanical transcription so you spend your attention on the logic that matters. Paste the JSON, sanity-check the inferred types against what the docs promise, and ship.
Made by Toolora · Updated 2026-06-13