JOS.JOSON 1.3.6

There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package JOS.JOSON --version 1.3.6
                    
NuGet\Install-Package JOS.JOSON -Version 1.3.6
                    
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="JOS.JOSON" Version="1.3.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JOS.JOSON" Version="1.3.6" />
                    
Directory.Packages.props
<PackageReference Include="JOS.JOSON" />
                    
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 JOS.JOSON --version 1.3.6
                    
#r "nuget: JOS.JOSON, 1.3.6"
                    
#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 JOS.JOSON@1.3.6
                    
#: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=JOS.JOSON&version=1.3.6
                    
Install as a Cake Addin
#tool nuget:?package=JOS.JOSON&version=1.3.6
                    
Install as a Cake Tool

JOS.JOSON

Fluent System.Text.Json configuration for domain objects, without attributes.

Configure how your types serialize and deserialize in separate configuration classes, keeping your domain models clean. Inspired by Entity Framework Core's IEntityTypeConfiguration<T>.

Installation

dotnet add package JOS.JOSON

Usage

Domain model

public class Order
{
    private decimal _originalAmount;

    public required Guid Id { get; init; }
    public required string CustomerName { get; init; }
    public required OrderStatus Status { get; init; }
    public required Money TotalAmount { get; init; }
    public required string InternalNotes { get; init; } = string.Empty;
    public required DateTime CreatedAt { get; init; }
    public decimal? Discount { get; init; }

    private Order() { }

    public static Order Create(
        string customerName,
        OrderStatus status,
        Money totalAmount,
        string internalNotes,
        decimal? discount = null) =>
        new()
        {
            Id = Guid.NewGuid(),
            CustomerName = customerName,
            Status = status,
            TotalAmount = totalAmount,
            InternalNotes = internalNotes,
            CreatedAt = DateTime.UtcNow,
            Discount = discount,
            _originalAmount = totalAmount.Amount
        };
}

public record Money(decimal Amount, string Currency);
public enum OrderStatus { Pending, Confirmed, Shipped, Delivered }

Configuration

public class OrderConfiguration : IJsonTypeConfiguration<Order>
{
    public void Configure(JsonEntityConfiguration<Order> builder)
    {
        builder
            // Private parameterless constructor is found automatically,
            // or call UsePrivateConstructor() to enforce it explicitly
            .Ignore(o => o.InternalNotes, RequiredIgnoreHandling.UseDefault)
            .PropertyName(o => o.CustomerName, "customer")
            .Converter(o => o.Status, new JsonStringEnumConverter())
            .Converter(o => o.TotalAmount, new MoneyConverter())
            .ShouldSerialize(o => o.Discount, (_, d) => d.HasValue && d.Value > 0)
            // Private fields must be referenced by name since lambda access is not possible
            .IncludeField("_originalAmount", f => f.PropertyName("original_amount"))
            .PropertyOrder(o => o.Id, 0);
    }
}

Setup

var options = new JOSONJsonTypeInfoResolver()
    .Apply(new OrderConfiguration())
    .BuildOptions();

// Or auto-discover all IJsonTypeConfiguration<T> in an assembly
var options = new JOSONJsonTypeInfoResolver()
    .ApplyConfigurationsFromAssembly(typeof(OrderConfiguration).Assembly)
    .BuildOptions();

Serialization

var order = Order.Create("Jane Doe", OrderStatus.Confirmed, new Money(149.99m, "USD"), "internal");
var json = JsonSerializer.Serialize(order, options);
{
    "id": "3fa85f64-...",
    "customer": "Jane Doe",
    "status": "Confirmed",
    "totalAmount": { "amount": 149.99, "currency": "USD" },
    "createdAt": "2024-01-15T10:30:00Z",
    "original_amount": 149.99
}

InternalNotes is excluded, CustomerName is serialized as customer, Status as a string, Discount is omitted because it has no value, and the private field _originalAmount is included using the string overload since private fields are not accessible via lambda.

Deserialization

The same options are used for deserialization. The configuration is applied in reverse - customer maps back to CustomerName, the MoneyConverter reconstructs the Money record, and the private constructor is used to create the instance.

var order = JsonSerializer.Deserialize<Order>(json, options);

Configuration API

Method Description
Ignore(selector) Exclude a property from serialization
PropertyName(selector, name) Rename a property in JSON
PropertyOrder(selector, order) Control position in JSON output
Converter(selector, converter) Apply a custom JsonConverter to a property
ShouldSerialize(selector, predicate) Conditionally include a property
NumberHandling(selector, handling) Control number parsing behaviour
ExtensionData(selector) Capture unknown JSON properties
IncludeField(selector) Include a public/internal field
IncludeField(name, configure?) Include a private field by name
IncludeProperty(selector) Include a property with a private setter
UsePrivateConstructor() Explicitly use a private parameterless constructor
UsePrivateSetterProperties() Include all properties with private setters
Property(selector) Access per-property configuration (e.g. HasConversion)
Polymorphism(configure) Configure polymorphic type handling
WithVersioning(configure) Enable JSON versioning with migration support

Private setter properties

Properties with private set are not included by default. Use IncludeProperty to opt in per property, or UsePrivateSetterProperties to include all of them globally.

public class Session
{
    public Instant Expires { get; private set; }
    public Instant? Updated { get; private set; }

    private Session() { }
}

Per property:

builder
    .UsePrivateConstructor()
    .IncludeProperty(x => x.Expires)
    .IncludeProperty(x => x.Updated);

// With inline conversion
builder
    .UsePrivateConstructor()
    .IncludeProperty(x => x.Expires, c => c.HasConversion(
        instant => instant.ToString(),
        value => Instant.Parse(value)));

All private setter properties:

builder
    .UsePrivateConstructor()
    .UsePrivateSetterProperties();

Inline conversion

Use Property(selector).HasConversion to control how a property is serialized and deserialized without writing a separate JsonConverter class. The first delegate converts to the stored type, the second converts back.

public class Session
{
    public DateOnly Date { get; init; }

    private Session() { }
}
// Via Property() for init properties
builder.Property(x => x.Date, c => c.HasConversion(
    date => date.ToString("yyyy-MM-dd"),
    value => DateOnly.Parse(value)));

// Via IncludeProperty() for private setter properties
builder.IncludeProperty(x => x.Date, c => c.HasConversion(
    date => date.ToString("yyyy-MM-dd"),
    value => DateOnly.Parse(value)));

The stored type (string here) is inferred from the serialize delegate's return type. Any type that STJ can handle natively can be used as the stored type - string, int, Guid, etc.

Polymorphism

Configure polymorphic deserialization on the base type:

public abstract class SessionMetadata { }
public class OidcSessionMetadata : SessionMetadata
{
    public required string ClientId { get; init; }
}
public class SessionMetadataConfiguration : IJsonTypeConfiguration<SessionMetadata>
{
    public void Configure(JsonEntityConfiguration<SessionMetadata> builder)
    {
        builder.Polymorphism(p => p
            .TypeDiscriminatorPropertyName("$type")
            .DerivedType<OidcSessionMetadata>("oidc"));
    }
}

The $type discriminator is written on serialization and used to resolve the correct derived type on deserialization.

Naming policy

Defaults to camelCase. Override with:

var options = new JOSONJsonTypeInfoResolver()
    .WithNamingPolicy(JsonNamingPolicy.SnakeCaseLower)
    .Apply(new OrderConfiguration())
    .BuildOptions();

Versioning

When you serialize an object and store it - in a database, a message queue, an event log - you are making a promise about its shape. The moment you rename a field, split a property, or change a type, you break readers of older data.

Without versioning the common workarounds are keeping old and new field names alive at the same time, sprinkling conditional logic across converters, or silently losing data. Versioning gives you a better option: stamp a version number on every document at write time, and repair any older shapes before they ever reach your deserializer.

How it works

A __metadata block is written alongside the payload:

{ "__metadata": { "version": 2 }, "customer": "Jane Doe", "totalAmount": "149.99 USD" }

On read, if the stored version is lower than the current version, migration functions run in order until the document matches the current shape. Your domain model only ever sees version 2.

Documents with no __metadata are treated as the current version, which covers the initial rollout when existing data was written before versioning was added.

Renaming a field (v1 to v2)

Version 1 used customerName. Version 2 renamed it to customer.

builder.WithVersioning(v => v
    .Version(2)
    .MigrateFrom(1, payload =>
    {
        var migrated = payload.GetRawText().Replace("\"customerName\"", "\"customer\"");
        return JsonDocument.Parse(migrated).RootElement.Clone();
    }));
Chained migrations (v1 to v3 via v2)

You never need to write a migration that jumps multiple versions. Each step only handles one increment, and the chain runs automatically.

builder.WithVersioning(v => v
    .Version(3)
    .MigrateFrom(1, payload =>  // 1 -> 2: rename customerName to customer
    {
        var migrated = payload.GetRawText().Replace("\"customerName\"", "\"customer\"");
        return JsonDocument.Parse(migrated).RootElement.Clone();
    })
    .MigrateFrom(2, payload =>  // 2 -> 3: rename amount to totalAmount
    {
        var migrated = payload.GetRawText().Replace("\"amount\"", "\"totalAmount\"");
        return JsonDocument.Parse(migrated).RootElement.Clone();
    }));

A document stored at version 1 will run both migrations. A document stored at version 2 will only run the second.

Retiring old versions

Once you are confident that no version 1 documents remain in storage, delete the MigrateFrom(1, ...) call. If a version 1 document turns up after that, it throws - which is the correct behaviour, since you have explicitly declared that version no longer supported.

Requirements

.NET 8 or later.

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.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net11.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

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
1.4.4-beta-gd052b7a7df 189 8/28/2026
1.4.3-beta-g2726e151cb 96 8/28/2026
1.4.2-beta-gcbf7fd77b5 99 8/28/2026
1.3.6 98 8/28/2026
1.3.5-beta-g83c47d958c 100 8/27/2026
1.3.4-beta-ge69b5aff82 94 8/27/2026
1.3.3-beta-g22e3fd0046 331 6/8/2026
1.3.2-beta-g0eb25a064c 111 6/8/2026
1.2.13 122 6/8/2026
1.2.11-beta-g1995cf3cc5 108 6/8/2026
1.2.9-beta-gbba29fdb98 108 6/8/2026
1.2.8-beta-geac922a4f7 108 6/8/2026
1.2.7-beta-g571247605e 115 6/4/2026
1.2.6-beta-gfe40f83e92 108 6/4/2026
1.2.5-beta-g787ad39885 101 6/4/2026
1.2.4-beta-g9e4ef7ed46 113 6/4/2026
1.2.3-beta-g05b764fd22 112 6/4/2026
1.2.2-beta-gaf074b8645 115 6/4/2026
1.1.2 121 6/4/2026
1.1.1-beta-gc133959bc0 112 6/4/2026
Loading failed