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
<PackageReference Include="Forge.Next.Formatters.Newtonsoft" Version="3.0.0" />
<PackageVersion Include="Forge.Next.Formatters.Newtonsoft" Version="3.0.0" />
<PackageReference Include="Forge.Next.Formatters.Newtonsoft" />
paket add Forge.Next.Formatters.Newtonsoft --version 3.0.0
#r "nuget: Forge.Next.Formatters.Newtonsoft, 3.0.0"
#:package Forge.Next.Formatters.Newtonsoft@3.0.0
#addin nuget:?package=Forge.Next.Formatters.Newtonsoft&version=3.0.0
#tool nuget:?package=Forge.Next.Formatters.Newtonsoft&version=3.0.0
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.
- Package version:
3.0.0 - Target frameworks:
net48,net8.0,net9.0,net10.0 - License: Apache-2.0
- Package dependencies:
Forge.Next.Formatters,Forge.Next.Shared,Newtonsoft.Json - Repository: https://github.com/JZO001/Forge.Next.Formatters.Newtonsoft
Table of contents
- Installation
- Why this library
- Core concepts
- Interfaces
- Classes and public members
- Dependency injection
- Recipes
- Error handling reference
- License
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], customJsonConverters,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.
net48support — the formatter targetsnet48in addition tonet8.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:
WriteAsyncserializes the object to JSON and writes it to the output stream.ReadAsync(inputStream)deserializes the JSON from the input stream into aT.
Note: As with the other serialization formatters in the Forge family, the second overload —
ReadAsync(inputStream, outputStream, …)— is intentionally not implemented and always returns aForbiddenerror 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-streamReadAsync(inputStream)does not pre-validate anullstream: anullargument surfaces as a caught (failure) result (IsError == true) rather than aValidationerror.WriteAsyncstill returns aValidationerror for anulldataoroutputStream.
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>.
Tcan 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 toJsonConvert(default: the value ofCreateDefaultSettings()).static JsonSerializerSettings CreateDefaultSettings()— creates a newJsonSerializerSettingsinstance 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)— deserializeTfrom the JSON ininputStream.Task<ErrorOr<Success>> ReadAsync(Stream inputStream, Stream outputStream, CancellationToken)— not implemented; always returns aForbiddenerror ("Method not implemented.").Task<ErrorOr<Success>> WriteAsync(T data, Stream outputStream, CancellationToken)— serializedatato JSON and write it tooutputStream. Returns aValidationerror whendataoroutputStreamisnull.
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…AsSingletonINewtonsoftJsonFormatter<>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
SerializerSettingsenablePreserveReferencesHandling.Objects. If a singleton formatter is shared across the whole app and you mutate itsSerializerSettingsat 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:
IsError—truewhen the operation failed.Value— the successful result (only valid whenIsErrorisfalse).FirstError— the firstError; use.Type,.Code,.Description.Errors— the full list ofErrorvalues.Match(...)/MatchFirst(...)/Switch(...)— functional handling.
License
Copyright © Zoltan Juhasz. Licensed under the Apache License 2.0.
| Product | Versions 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. |
-
.NETFramework 4.8
- Forge.Next.Formatters (>= 3.2.1)
- Newtonsoft.Json (>= 13.0.4)
-
net10.0
- Forge.Next.Formatters (>= 3.2.1)
- Newtonsoft.Json (>= 13.0.4)
-
net8.0
- Forge.Next.Formatters (>= 3.2.1)
- Newtonsoft.Json (>= 13.0.4)
-
net9.0
- Forge.Next.Formatters (>= 3.2.1)
- Newtonsoft.Json (>= 13.0.4)
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 |