Skip to content

Serialisation Helpers

m4bwav edited this page Sep 30, 2026 · 3 revisions

ToJson() and DeserializeFromJson<T>() in the namespace JsonPrettyPrinterPlus.JsonSerialization are thin wrappers over System.Text.Json.JsonSerializer. They exist so that "object to readable JSON" is one call. They have run on System.Text.Json since 2.0.0; before that they used JavaScriptSerializer, and Versions and upgrading lists what that changed.

ToJson

using JsonPrettyPrinterPlus.JsonSerialization;

var thing = new { Name = "Mark", Tags = new[] { "a", "b" }, When = new DateTime(2010, 1, 1), Nothing = (string?)null, Price = 9.5m };

thing.ToJson();
// {"Name":"Mark","Tags":["a","b"],"When":"2010-01-01T00:00:00","Nothing":null,"Price":9.5}

thing.ToJson(prettyPrint: true);
{
    "Name": "Mark",
    "Tags": [
        "a",
        "b"
    ],
    "When": "2010-01-01T00:00:00",
    "Nothing": null,
    "Price": 9.5
}

Property names keep their C# casing, a DateTime is ISO 8601 (with an offset for a DateTimeOffset: 2010-01-01T00:00:00-06:00), a Guid is its usual text, an enum is its number, and a null property is null. ((object?)null).ToJson() is the text null. The object is serialised as its runtime type, so a base-typed variable still writes every property of the actual object. Dictionaries and lists work as you would expect.

With your own options

using System.Text.Json;

var camel = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
thing.ToJson(camel, prettyPrint: true);
{
    "name": "Mark",
    "tags": [
        "a",
        "b"
    ],
    "when": "2010-01-01T00:00:00",
    "nothing": null,
    "price": 9.5
}

Anything System.Text.Json accepts goes in the options: naming policies, converters, DefaultIgnoreCondition, enums as strings. Passing null means the library defaults. WriteIndented = true in your options is honoured, but it gives System.Text.Json's layout (two spaces, Environment.NewLine); prettyPrint: true gives this library's and wins if both are set.

Escaping

By default System.Text.Json escapes ", ', <, >, & and every character outside ASCII as \uXXXX:

new { s = "<b>&'é\"</b>" }.ToJson();
// {"s":"\u003Cb\u003E\u0026\u0027\u00E9\u0022\u003C/b\u003E"}

That is valid JSON and every parser reads it back, but it is not what you want to look at. Relax it:

using System.Text.Encodings.Web;

var readable = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
new { s = "<b>&'é\"</b>" }.ToJson(readable);
// {"s":"<b>&'é\"</b>"}

The "unsafe" in the name means the output is not safe to paste into HTML or JavaScript without further encoding. For files, logs and APIs it is the encoder you want.

Source generation, trimming and native AOT

The overloads without a JsonTypeInfo<T> use reflection. On net10.0 they carry RequiresUnreferencedCode and RequiresDynamicCode, so a project with PublishTrimmed or PublishAot gets warnings IL2026 and IL3050 at every call, and under native AOT the call fails at run time with InvalidOperationException: Reflection-based serialization has been disabled for this application. Use a JsonSerializerContext instead:

using System.Text.Json.Serialization;

public sealed class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public DateTime Born { get; set; }
    public string[] Tags { get; set; } = Array.Empty<string>();
}

[JsonSerializable(typeof(Person))]
internal partial class PersonContext : JsonSerializerContext
{
}

var person = new Person { Name = "Ada", Age = 36, Born = new DateTime(1815, 12, 10), Tags = new[] { "math" } };
var json = person.ToJson(PersonContext.Default.Person, prettyPrint: true);
var back = json.DeserializeFromJson(PersonContext.Default.Person);
{
    "Name": "Ada",
    "Age": 36,
    "Born": "1815-12-10T00:00:00",
    "Tags": [
        "math"
    ]
}

.NET 10 file-based apps (dotnet run app.cs) enable native AOT by default, so the reflection overloads throw there even under dotnet run. Either use a context as above, or put #:property PublishAot=false at the top of the file.

DeserializeFromJson

var person = """{"Name":"Ada","Age":36,"Born":"1815-12-10T00:00:00","Tags":["math"]}""".DeserializeFromJson<Person>();

Property matching is case-sensitive by default. For camel-cased input, pass options:

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var person = """{"name":"Ada","age":36}""".DeserializeFromJson<Person>(options);

DeserializeFromJson<object>() returns a JsonElement, and DeserializeFromJson<Dictionary<string, JsonElement>>() a dictionary of them. The text null deserialises to null for a reference type. Text that is not JSON throws JsonException with the position, for example 'n' is an invalid start of a property name. Expected a '"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1. Null text throws ArgumentNullException.

Numbers on .NET Framework

On .NET Framework the package loads its netstandard2.0 build, and the System.Text.Json package there formats floating-point numbers differently from .NET 10. From the package's golden recordings of 3.0.1:

Value .NET Framework 4.8 .NET 10
0.1.ToJson() 0.10000000000000001 0.1
1e300.ToJson() 1.0000000000000001E+300 1E+300
(-0.0).ToJson() 0 -0
0.1f.ToJson() 0.100000001 0.1
"1e400".DeserializeFromJson<double>() throws JsonException Infinity

Both forms read back as the same number. Compare parsed values, not text, when the same code runs on both.

Where System.Text.Json comes from

On net10.0 the helpers use the System.Text.Json in the framework and the package has no dependencies. On netstandard2.0 the package references System.Text.Json 10.0.12, so .NET Framework and older .NET Core projects get it, and its own dependencies, from NuGet. That reference is the only reason the netstandard2.0 build has a dependency tree; the printer itself needs nothing beyond System.Memory. Keeping the helpers on netstandard2.0 was a deliberate choice for 3.0, because the projects most likely to want a one-liner are the older ones.

Other serialisers

Nothing ties the printer to System.Text.Json. Any serialiser's compact output can go through PrettyPrintJson(): Recipes has Newtonsoft.Json and PowerShell examples.

Clone this wiki locally