DevKitHub

Programming

JSON to C# Class and Record Converter

Paste a JSON response to get C# classes or records with a JsonPropertyName or JsonProperty attribute on every property. Every element of an array is merged into one class, so optional and nullable properties come out right.

21 lines
53 lines

Deserialize

Root type
List<RootItem>
  • $[].placed_at: every sample is an ISO 8601 date-time with a zone, so the type is DateTimeOffset. Deserializing fails on anything else, including ""; use string if the API can send those.
  • $[].delivery_date: every sample is a date without a time, so the type is DateOnly, which both serializers read and write as yyyy-MM-dd (Newtonsoft.Json from version 13.0.2).

The classes describe the sample you paste: a key in every sample is required, so paste several responses to find the optional ones. Needs C# 11 and .NET 7 or later.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

Every value found at the same position is merged into one description before any C# is written, and what the samples show is what the types promise. The file starts with #nullable enable, so string means never null and string? means may be null whatever the project setting, and the compiler flags code that forgets to check. A key present in every sample is a required property (C# 11): code that builds the object with new must set it, and System.Text.Json, from .NET 7, refuses a document that leaves it out, with "JSON deserialization for type 'Root' was missing required properties". Newtonsoft.Json ignores the keyword, so for it the attribute carries the same rule, Required = Required.Always, or Required.AllowNull where some sample was null. A key missing from some samples is an optional nullable property, such as long? or string?, left null when absent and written back as null.

Integers are long, not int, because a sample says nothing about the largest value still to come. A fraction or an exponent makes a property double, which is binary: 0.1 + 0.2 is 0.30000000000000004, and 19.99 is stored as the nearest double. For money choose decimal, which is base 10 and keeps 19.99, and even the trailing zero of 1.50, exactly; both serializers write 1.50 back. An integer beyond long is decimal, which holds 28 digits. A string property is DateTimeOffset only when every sample is an ISO 8601 date-time with a Z or an offset. DateTimeOffset keeps the offset; DateTime turns 10:00+05:30 into the local time of whichever machine reads it, DateTimeKind.Local, so two servers read two different values from the same JSON. A time with no zone stays string, because both serializers would quietly attach the reader’s own offset, and a date on its own is DateOnly. System.Text.Json refuses a lowercase t or z that Newtonsoft.Json accepts, so only the strict form is typed as a date.

Property names are PascalCase from the key, keeping its own capitals as the .NET naming guidelines do: user_id, user-id and userId all give UserId, and the attribute keeps the original key, [JsonPropertyName("user_id")] or [JsonProperty("user_id")]. The attribute matters more than it looks, because by default System.Text.Json matches names case-sensitively and would never put "user_id" into UserId, while Newtonsoft.Json matches case-insensitively. C# forbids a property with the name of its own class, error CS0542, and JSON produces that easily, as in {"status": {"status": "shipped"}}; such a property is renamed StatusValue, with a warning. So is a name every object already has, such as ToString or Equals, which in a record is a compile error. Keys that give the same name are numbered UserId2, a keyword in the namespace is escaped with @, and a digit at the start gets Field in front. Records are init-only, { get; init; }, rather than positional, so they carry the same attributes and required checks as the classes. Nested objects are separate classes, shared when identical, and a root array has no class of its own: deserialize it as the List<T> the tool shows.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

JSON does not allow comments.

{
  // Logging
  "Logging": { "LogLevel": { "Default": "Information" } }
}
Why:
Comments, as in appsettings.json, which .NET’s configuration reader accepts along with trailing commas. They are not JSON: JsonSerializer rejects them unless ReadCommentHandling is set to Skip.
Fix:
Delete the comments before pasting.

JSON keys must use double quotes; single quotes are JavaScript, not JSON.

{'id': 1, 'name': 'Ada'}
Why:
Single-quoted keys or strings, often from a JavaScript object or a hand-written test string. Newtonsoft.Json reads them, which is why such JSON can seem to work; System.Text.Json and strict parsers do not.
Fix:
Replace the single quotes with double quotes.

JSON deserialization for type 'Root' was missing required properties including: 'email'.

Why:
Every sample you pasted had the key, so the property is required, and System.Text.Json refuses a real response that leaves it out. One response cannot show that a key is optional.
Fix:
Paste several responses, including one without the key, or remove required and make the type nullable by hand.

The JSON value could not be converted to System.Int64.

Why:
The samples held only whole numbers, so the property is long, and the API also sends fractions. System.Text.Json refuses even 1.0 for a long, while Newtonsoft.Json quietly turns 1.5 into 2.
Fix:
Paste a sample with a fractional value, or change the type to double, or to decimal for money.

A date-time comes back hours out on another server.

Why:
The property was generated as DateTime. Both serializers convert a time with an offset, such as 10:00+05:30, to the local time of the machine that reads it, so machines in different zones read different values.
Fix:
Keep the default DateTimeOffset, which keeps the offset, or convert to UTC as soon as the value is read.

Frequently asked questions

Should I use System.Text.Json or Newtonsoft.Json?
System.Text.Json is part of .NET and is what ASP.NET Core uses by default, so start there. Newtonsoft.Json is a NuGet package that is more lenient, reading comments, single quotes and names in any case, and is common in older code. The generated classes differ only in the attributes and the using line.
Which C# and .NET version does the output need?
C# 11 and .NET 7 or later: required members are C# 11, and System.Text.Json enforces them from .NET 7. The file-scoped namespace needs C# 10 and init needs C# 9. For an older compiler, remove required and wrap the classes in a namespace block.
Why is there no Root class when the JSON is an array?
Because the document is a list, not an object with properties. Deserialize it as the type shown, for example JsonSerializer.Deserialize<List<RootItem>>(json), and each element is a RootItem.
Should I use a class or a record?
A record compares by value, so two orders with the same properties are equal, and with its init-only properties it cannot be changed after it is read; with expressions give changed copies. A class compares by reference and can be updated in place. Both deserialize the same way.

Last updated