Forge.Next.Formatters.Newtonsoft 3.0.0

dotnet add package Forge.Next.Formatters.Newtonsoft --version 3.0.0
                    
NuGet\Install-Package Forge.Next.Formatters.Newtonsoft -Version 3.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Forge.Next.Formatters.Newtonsoft" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Forge.Next.Formatters.Newtonsoft" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Forge.Next.Formatters.Newtonsoft" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Forge.Next.Formatters.Newtonsoft --version 3.0.0
                    
#r "nuget: Forge.Next.Formatters.Newtonsoft, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Forge.Next.Formatters.Newtonsoft@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Forge.Next.Formatters.Newtonsoft&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Forge.Next.Formatters.Newtonsoft&version=3.0.0
                    
Install as a Cake Tool

Forge.Next.Formatters.Newtonsoft

Reference, practice and pattern implementation of a Newtonsoft.Json (Json.NET) data formatter for .NET. It is the Json.NET counterpart of SystemJsonFormatter<T> from Forge.Next.Formatters: it implements the very same IDataFormatter<T> contract, serializes objects to JSON and back with Newtonsoft.Json, and never throws for expected failures — it returns an ErrorOr<T> result instead.


Table of contents


Installation

Install the package from NuGet:

dotnet add package Forge.Next.Formatters.Newtonsoft

Then import the namespaces you need (the formatter, the Newtonsoft.Json settings types, and — when you want to inspect results — the ErrorOr namespace):

using Forge.Next.Formatters.Newtonsoft;
using Newtonsoft.Json;
using ErrorOr;

Why this library

Forge.Next.Formatters already ships a JSON formatter (SystemJsonFormatter<T>) built on System.Text.Json. This package adds a drop-in alternative built on Newtonsoft.Json, for the cases where you need Json.NET specifically:

  • Json.NET features[JsonProperty], [JsonIgnore], custom JsonConverters, PreserveReferencesHandling, TypeNameHandling, DefaultValueHandling, and the large ecosystem of Json.NET-based converters and contracts.
  • Existing Json.NET models — types already annotated for Newtonsoft.Json serialize identically.
  • net48 support — the formatter targets net48 in addition to net8.0/net9.0/net10.0.

The formatter implements the same IDataFormatter<T> contract as every other Forge formatter, so it composes with the helpers in Forge.Next.Formatters (for example the GZip-aware DataFormatterExtensions.Read / Write) exactly like the built-in formatters do.

Formatter Payload type T Engine What it does
NewtonsoftJsonFormatter<T> T Newtonsoft.Json JSON serialization / deserialization

All operations are asynchronous and return an ErrorOr<...> value, so failures (a null argument, malformed JSON, an unmatched type, …) are surfaced as data instead of exceptions.


Core concepts

The IDataFormatter<T> contract

NewtonsoftJsonFormatter<T> implements IDataFormatter<T> (from Forge.Next.Formatters), which has exactly three methods:

public interface IDataFormatter<T>
{
    // Read/decode the whole input stream and return the produced object.
    Task<ErrorOr<T?>> ReadAsync(Stream inputStream, CancellationToken cancellationToken = default);

    // Read/decode the input stream and write the produced bytes to the output stream.
    Task<ErrorOr<Success>> ReadAsync(Stream inputStream, Stream outputStream, CancellationToken cancellationToken = default);

    // Write/encode the object into the output stream.
    Task<ErrorOr<Success>> WriteAsync(T data, Stream outputStream, CancellationToken cancellationToken = default);
}

For this serialization formatter:

  • WriteAsync serializes the object to JSON and writes it to the output stream.
  • ReadAsync(inputStream) deserializes the JSON from the input stream into a T.

Note: As with the other serialization formatters in the Forge family, the second overload — ReadAsync(inputStream, outputStream, …) — is intentionally not implemented and always returns a Forbidden error with the description "Method not implemented.".

Working with ErrorOr<T> results

None of the public methods throw for an expected failure. Instead they return an ErrorOr<T>. The two patterns you will use most:

ErrorOr<Person?> result = await formatter.ReadAsync(stream);

// 1) Imperative check
if (result.IsError)
{
    Error error = result.FirstError;
    Console.WriteLine($"Failed: {error.Type} - {error.Description}");
}
else
{
    Person? value = result.Value;
    // use value...
}

// 2) Functional Match (from the ErrorOr package)
string message = result.Match(
    value => $"Got {value?.Name}",
    errors => $"Failed: {errors[0].Description}");

The formatter produces three kinds of results:

Error type When
Validation WriteAsync was called with a null data or null outputStream. The Description is the offending parameter name — "data" or "outputStream".
Forbidden The unimplemented ReadAsync(inputStream, outputStream) overload. Description is "Method not implemented.".
(failure) Any exception raised inside the operation (malformed JSON, a null input stream, a type mismatch, …) is caught and returned as an errored result — IsError is true — instead of propagating.

Internally the "catch every exception and turn it into an errored result" behavior is provided by the ProtectAsync wrapper from Forge.Next.Shared, which every operation runs inside. You never call it directly; it is the reason malformed JSON becomes result.IsError == true rather than a thrown JsonException.

Like SystemJsonFormatter<T>, the single-stream ReadAsync(inputStream) does not pre-validate a null stream: a null argument surfaces as a caught (failure) result (IsError == true) rather than a Validation error. WriteAsync still returns a Validation error for a null data or outputStream.

The default JsonSerializerSettings

Every NewtonsoftJsonFormatter<T> starts with a fresh JsonSerializerSettings instance produced by CreateDefaultSettings():

new JsonSerializerSettings
{
    DateFormatHandling         = DateFormatHandling.IsoDateFormat,
    DefaultValueHandling       = DefaultValueHandling.Populate,
    Formatting                 = Formatting.None,
    NullValueHandling          = NullValueHandling.Include,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects
};
Setting Value Effect
DateFormatHandling IsoDateFormat Dates are written/read in ISO 8601 (2026-07-12T10:20:30Z).
DefaultValueHandling Populate On deserialization, members absent from the JSON are set to their default value.
Formatting None Compact output, no indentation.
NullValueHandling Include null members are written to the JSON.
PreserveReferencesHandling Objects Object references are tracked with $id/$ref metadata so shared/circular references round-trip.

You can replace the whole SerializerSettings object, or start from the defaults and tweak individual properties — see Customize the serializer settings.

PreserveReferencesHandling and the $id metadata

Because the default settings enable PreserveReferencesHandling.Objects, the serialized JSON contains Json.NET reference-tracking metadata ("$id", and "$ref" for repeated references):

{"$id":"1","Name":"Ada","Age":36}

This is expected. It lets object graphs with shared or circular references round-trip correctly (see Round-trip circular object references). If you are producing JSON for a third party that does not understand $id/$ref, turn the feature off:

var formatter = new NewtonsoftJsonFormatter<Person>
{
    SerializerSettings = new JsonSerializerSettings
    {
        PreserveReferencesHandling = PreserveReferencesHandling.None
    }
};

A note about streams and Position

WriteAsync writes to the output stream you supply and does not close it; ReadAsync likewise reads the input stream without closing it (its internal StreamReader is created with leaveOpen: true). When you write into a MemoryStream and then read it back with the same (or another) formatter, rewind it first:

using var buffer = new MemoryStream();
await formatter.WriteAsync(person, buffer);

buffer.Position = 0;                     // <-- rewind before reading
ErrorOr<Person?> read = await formatter.ReadAsync(buffer);

System.Text.Json vs. Newtonsoft.Json

NewtonsoftJsonFormatter<T> is API-compatible with SystemJsonFormatter<T> — both implement IDataFormatter<T> and expose a settings/options property — but they are configured with different types:

SystemJsonFormatter<T> NewtonsoftJsonFormatter<T>
Engine System.Text.Json Newtonsoft.Json
Interface ISystemJsonFormatter<T> INewtonsoftJsonFormatter<T>
Configuration property JsonSerializerOptions SerializerOptions JsonSerializerSettings SerializerSettings
Default configuration JsonSerializerOptions.Default CreateDefaultSettings()
DI (scoped) AddForgeFormattersAsScoped() AddForgeNewtonsoftJsonFormatterAsScoped()

Newtonsoft.Json has no native asynchronous (de)serialization, so in this formatter the stream I/O is performed asynchronously while the (de)serialization itself is synchronous. The public methods are still async and honor the CancellationToken.


Interfaces

The formatter is registered and injected through its interface:

Interface Extends Adds Implemented by
IDataFormatter<T> ReadAsync (×2), WriteAsync (all Forge formatters)
INewtonsoftJsonFormatter<T> IDataFormatter<T> SerializerSettings NewtonsoftJsonFormatter<T>

Program against INewtonsoftJsonFormatter<T> (it is what the AddForgeNewtonsoftJsonFormatter… methods register) and let the container hand you the concrete implementation.


Classes and public members

INewtonsoftJsonFormatter<T>

The abstraction for the Newtonsoft.Json formatter. Extends IDataFormatter<T> and adds the Json.NET configuration surface.

public interface INewtonsoftJsonFormatter<T> : IDataFormatter<T>
{
    JsonSerializerSettings SerializerSettings { get; set; }
}

Public members

  • JsonSerializerSettings SerializerSettings { get; set; } — the Json.NET settings used for both serialization and deserialization.
  • Inherited from IDataFormatter<T>: ReadAsync(Stream, …), ReadAsync(Stream, Stream, …), WriteAsync(T, Stream, …).

Example — depend on the interface

using Forge.Next.Formatters.Newtonsoft;
using Newtonsoft.Json;
using ErrorOr;

public sealed class ProfileSerializer
{
    private readonly INewtonsoftJsonFormatter<Person> _formatter;

    // Injected by the DI container (see "Dependency injection").
    public ProfileSerializer(INewtonsoftJsonFormatter<Person> formatter)
    {
        _formatter = formatter;

        // The settings are configurable through the interface:
        _formatter.SerializerSettings.Formatting = Formatting.Indented;
    }

    public async Task<byte[]> SaveAsync(Person person, CancellationToken ct)
    {
        using var buffer = new MemoryStream();
        ErrorOr<Success> result = await _formatter.WriteAsync(person, buffer, ct);
        if (result.IsError)
            throw new InvalidOperationException(result.FirstError.Description);

        return buffer.ToArray();
    }
}

NewtonsoftJsonFormatter<T>

Serializes / deserializes an object of type T to JSON using Newtonsoft.Json.JsonConvert. Implements INewtonsoftJsonFormatter<T> : IDataFormatter<T>.

T can be any type Json.NET can serialize (public types with public read/write members, Json.NET-annotated types, collections, etc.).

Public members

  • JsonSerializerSettings SerializerSettings { get; set; } — the settings passed to JsonConvert (default: the value of CreateDefaultSettings()).
  • static JsonSerializerSettings CreateDefaultSettings() — creates a new JsonSerializerSettings instance carrying the documented defaults. A fresh instance is returned on every call, so callers may mutate it without affecting other formatter instances.
  • Task<ErrorOr<T?>> ReadAsync(Stream inputStream, CancellationToken) — deserialize T from the JSON in inputStream.
  • Task<ErrorOr<Success>> ReadAsync(Stream inputStream, Stream outputStream, CancellationToken)not implemented; always returns a Forbidden error ("Method not implemented.").
  • Task<ErrorOr<Success>> WriteAsync(T data, Stream outputStream, CancellationToken) — serialize data to JSON and write it to outputStream. Returns a Validation error when data or outputStream is null.
SerializerSettings
using Forge.Next.Formatters.Newtonsoft;
using Newtonsoft.Json;

var formatter = new NewtonsoftJsonFormatter<Person>();

// Inspect the defaults...
Formatting f = formatter.SerializerSettings.Formatting; // Formatting.None

// ...or replace them wholesale.
formatter.SerializerSettings = new JsonSerializerSettings
{
    Formatting = Formatting.Indented,
    NullValueHandling = NullValueHandling.Ignore
};
CreateDefaultSettings()

Use this when you want the library defaults as a starting point and then adjust a few properties:

using Forge.Next.Formatters.Newtonsoft;
using Newtonsoft.Json;

JsonSerializerSettings settings = NewtonsoftJsonFormatter<Person>.CreateDefaultSettings();
settings.Formatting = Formatting.Indented; // keep the other defaults, change only formatting

var formatter = new NewtonsoftJsonFormatter<Person> { SerializerSettings = settings };
WriteAsync / ReadAsync — full round-trip
using Forge.Next.Formatters.Newtonsoft;
using ErrorOr;

public class Person
{
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
}

var formatter = new NewtonsoftJsonFormatter<Person>();
var person = new Person { Name = "Ada", Age = 36 };

// Serialize to JSON
using var json = new MemoryStream();
ErrorOr<Success> write = await formatter.WriteAsync(person, json);
if (write.IsError) throw new InvalidOperationException(write.FirstError.Description);

// Deserialize (rewind first!)
json.Position = 0;
ErrorOr<Person?> read = await formatter.ReadAsync(json);
Person restored = read.Value!;   // Name = "Ada", Age = 36
ReadAsync(inputStream, outputStream) — not implemented
var forbidden = await formatter.ReadAsync(new MemoryStream(), new MemoryStream());
// forbidden.IsError == true
// forbidden.FirstError.Type == ErrorType.Forbidden
// forbidden.FirstError.Description == "Method not implemented."
WriteAsync — validation of null arguments
using ErrorOr;

// null data -> Validation error, Description = "data"
ErrorOr<Success> nullData = await formatter.WriteAsync(null!, new MemoryStream());
// nullData.FirstError.Type == ErrorType.Validation, Description == "data"

// null output stream -> Validation error, Description = "outputStream"
ErrorOr<Success> nullOut = await formatter.WriteAsync(new Person(), null!);
// nullOut.FirstError.Type == ErrorType.Validation, Description == "outputStream"

ServiceCollectionExtensions

Registers the Newtonsoft.Json formatter with the Microsoft dependency-injection container. Two entry points are provided that differ only in the registered lifetime. Both register the open generic INewtonsoftJsonFormatter<>NewtonsoftJsonFormatter<>, so you can resolve it for any T.

Public members

  • IServiceCollection AddForgeNewtonsoftJsonFormatterAsScoped(this IServiceCollection services) — registers the formatter as scoped and returns the same collection.

  • IServiceCollection AddForgeNewtonsoftJsonFormatterAsSingleton(this IServiceCollection services) — registers the formatter as a singleton and returns the same collection.

    Service Implementation …AsScoped …AsSingleton
    INewtonsoftJsonFormatter<> NewtonsoftJsonFormatter<> Scoped Singleton

Example

using Forge.Next.Formatters.Newtonsoft;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddForgeNewtonsoftJsonFormatterAsScoped(); // or AddForgeNewtonsoftJsonFormatterAsSingleton()

using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();

var json = scope.ServiceProvider.GetRequiredService<INewtonsoftJsonFormatter<Person>>();
// json is a NewtonsoftJsonFormatter<Person>

Dependency injection

The AddForgeNewtonsoftJsonFormatter… methods are designed for ASP.NET Core / generic-host applications. Register once at startup and inject INewtonsoftJsonFormatter<T> where you need it. Pick …AsSingleton when the formatter's configuration is fixed for the whole application, or …AsScoped when you configure per-scope state (for example a per-request SerializerSettings).

using Forge.Next.Formatters.Newtonsoft;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddForgeNewtonsoftJsonFormatterAsSingleton();
// It composes with the base package too:
// builder.Services.AddForgeFormattersAsScoped();

var app = builder.Build();
using Forge.Next.Formatters.Newtonsoft;
using ErrorOr;

public sealed class DocumentStore
{
    private readonly INewtonsoftJsonFormatter<Document> _json;

    // The formatter is injected by the container.
    public DocumentStore(INewtonsoftJsonFormatter<Document> json) => _json = json;

    public async Task<byte[]> SerializeAsync(Document doc, CancellationToken ct)
    {
        using var buffer = new MemoryStream();
        ErrorOr<Success> result = await _json.WriteAsync(doc, buffer, ct);
        if (result.IsError)
            throw new InvalidOperationException(result.FirstError.Description);

        return buffer.ToArray();
    }
}

The formatter's default SerializerSettings enable PreserveReferencesHandling.Objects. If a singleton formatter is shared across the whole app and you mutate its SerializerSettings at runtime, that change is global. Prefer configuring the settings once at registration time, or use a scoped lifetime when you need per-scope configuration.


Recipes

Customize the serializer settings

using Forge.Next.Formatters.Newtonsoft;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

var formatter = new NewtonsoftJsonFormatter<Person>
{
    SerializerSettings = new JsonSerializerSettings
    {
        Formatting = Formatting.Indented,
        NullValueHandling = NullValueHandling.Ignore,
        ContractResolver = new CamelCasePropertyNamesContractResolver()
    }
};

using var buffer = new MemoryStream();
await formatter.WriteAsync(new Person { Name = "Ada", Age = 36 }, buffer);
// JSON is indented and uses camelCased property names ("name", "age").

Round-trip circular object references

The default PreserveReferencesHandling.Objects lets object graphs with shared or circular references serialize and deserialize correctly.

using Forge.Next.Formatters.Newtonsoft;
using ErrorOr;

public class Node
{
    public string Name { get; set; } = string.Empty;
    public Node? Next { get; set; }
}

var a = new Node { Name = "A" };
var b = new Node { Name = "B" };
a.Next = b;
b.Next = a;                      // circular reference

var formatter = new NewtonsoftJsonFormatter<Node>(); // defaults preserve references

using var buffer = new MemoryStream();
await formatter.WriteAsync(a, buffer);               // does not loop forever

buffer.Position = 0;
ErrorOr<Node?> read = await formatter.ReadAsync(buffer);
Node restored = read.Value!;
// restored.Next!.Next is the same instance as restored (cycle rebuilt via $ref)

Use a custom JsonConverter

using Forge.Next.Formatters.Newtonsoft;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

var formatter = new NewtonsoftJsonFormatter<Order>();

// Serialize enums as strings, for example.
formatter.SerializerSettings.Converters.Add(new StringEnumConverter());

using var buffer = new MemoryStream();
await formatter.WriteAsync(new Order { Status = OrderStatus.Shipped }, buffer);
// "Status":"Shipped" instead of "Status":2

Compress JSON with GZip

Because NewtonsoftJsonFormatter<T> is an IDataFormatter<T>, the GZip-aware extension methods from Forge.Next.Formatters work on it directly.

using Forge.Next.Formatters;            // DataFormatterExtensions
using Forge.Next.Formatters.Newtonsoft; // NewtonsoftJsonFormatter<T>
using ErrorOr;

var formatter = new NewtonsoftJsonFormatter<Person>();
var person = new Person { Name = "Ada", Age = 36 };

// Serialize to JSON AND GZip-compress into a stream (compress: true is the default).
using var buffer = new MemoryStream();
await formatter.Write(person, buffer);       // extension method from the base package

// Read back: GZip-decompress AND deserialize (decompress: true is the default).
buffer.Position = 0;
ErrorOr<Person?> read = await formatter.Read(buffer);
Person restored = read.Value!;

Persist an object to a compressed file

using Forge.Next.Formatters;            // DataFormatterExtensions
using Forge.Next.Formatters.Newtonsoft;
using ErrorOr;

var formatter = new NewtonsoftJsonFormatter<Person>();
var file = new FileInfo("people/ada.json.gz");
file.Directory!.Create();

var person = new Person { Name = "Ada", Age = 36 };

// Serialize to JSON, GZip-compress, and write to the file in one call.
ErrorOr<Success> saved = await formatter.Write(person, file);   // compress: true (default)

// Load: decompress and deserialize in one call.
ErrorOr<Person?> loaded = await formatter.Read(file);           // decompress: true (default)
Person restored = loaded.Value!;

Error handling reference

Every method returns an ErrorOr<...>; inspect it instead of catching exceptions.

using Forge.Next.Formatters.Newtonsoft;
using ErrorOr;

var formatter = new NewtonsoftJsonFormatter<Person>();

// null data -> Validation error, Description = parameter name
ErrorOr<Success> nullData = await formatter.WriteAsync(null!, new MemoryStream());
// nullData.IsError == true
// nullData.FirstError.Type == ErrorType.Validation
// nullData.FirstError.Description == "data"

// Unimplemented stream-to-stream read -> Forbidden
ErrorOr<Success> forbidden = await formatter.ReadAsync(new MemoryStream(), new MemoryStream());
// forbidden.FirstError.Type == ErrorType.Forbidden
// forbidden.FirstError.Description == "Method not implemented."

// Malformed JSON -> errored result (not an exception)
using var garbage = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("this is not json"));
ErrorOr<Person?> corrupt = await formatter.ReadAsync(garbage);
// corrupt.IsError == true

Handy ErrorOr<T> members:

  • IsErrortrue when the operation failed.
  • Value — the successful result (only valid when IsError is false).
  • FirstError — the first Error; use .Type, .Code, .Description.
  • Errors — the full list of Error values.
  • Match(...) / MatchFirst(...) / Switch(...) — functional handling.

License

Copyright © Zoltan Juhasz. Licensed under the Apache License 2.0.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Framework net48 is compatible.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 86 7/12/2026