DataNormalizer 0.1.4

dotnet add package DataNormalizer --version 0.1.4
                    
NuGet\Install-Package DataNormalizer -Version 0.1.4
                    
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="DataNormalizer" Version="0.1.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DataNormalizer" Version="0.1.4" />
                    
Directory.Packages.props
<PackageReference Include="DataNormalizer" />
                    
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 DataNormalizer --version 0.1.4
                    
#r "nuget: DataNormalizer, 0.1.4"
                    
#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 DataNormalizer@0.1.4
                    
#: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=DataNormalizer&version=0.1.4
                    
Install as a Cake Addin
#tool nuget:?package=DataNormalizer&version=0.1.4
                    
Install as a Cake Tool

DataNormalizer

Compile-time graph normalization for .NET. Generate flat, deduplicated API contracts from nested object graphs.

CI NuGet License: MIT

Documentation | API Reference

The Problem

APIs that return nested object trees repeat the same entities over and over. A search response with 20 routes through 5 airports serializes each airport up to 40 times. That costs bytes on the wire, redundant parsing work on every client, and hand-written flatten/rehydrate code to clean it up.

What DataNormalizer Does

DataNormalizer is a source generator. You point it at your root type and it produces flat, deduplicated DTOs, a typed container, and Normalize/Denormalize methods — all at compile time, zero reflection.

Given a transport search response with routes, hops, carriers, and places, the normalized JSON looks like this:

{
  "result": {
    "routesIndices": [0, 1],
    "originIndex": 0,
    "destinationIndex": 1
  },
  "routeDtos": [
    { "name": "Fly", "hopsIndices": [0, 1] },
    { "name": "Train", "hopsIndices": [2] }
  ],
  "hopDtos": [
    { "carrierIndex": 0, "departureIndex": 0, "arrivalIndex": 2, "durationMinutes": 30 },
    { "carrierIndex": 1, "departureIndex": 2, "arrivalIndex": 1, "durationMinutes": 85 },
    { "carrierIndex": 2, "departureIndex": 0, "arrivalIndex": 1, "durationMinutes": 660 }
  ],
  "carrierDtos": [
    { "name": "SkyBus", "code": "SKYBUS" },
    { "name": "Qantas", "code": "QF" },
    { "name": "NSW TrainLink", "code": "XPT" }
  ],
  "placeDtos": [
    { "name": "Melbourne", "lat": -37.814, "lng": 144.963 },
    { "name": "Sydney", "lat": -33.865, "lng": 151.207 },
    { "name": "Melbourne Airport", "lat": -37.670, "lng": 144.849 }
  ]
}

Melbourne appears once. Carriers are stored once. Each hop references them by index.

Normalization + Gzip

"Just gzip it" removes syntactic redundancy, but normalization removes semantic redundancy first — then gzip compresses what's left even further.

Measured on a real transport search API response:

Format Raw Gzipped
Normalized 345 KB 58 KB
Unnormalized 713 KB 121 KB

Raw savings: 368 KB (2.1x smaller). Gzipped savings: 63 KB (2.1x smaller).

Normalize first, gzip second. See Why Gzip Isn't Enough for the full analysis.

Quick Start

dotnet add package DataNormalizer

1. Define your types

public class SearchResponse
{
    public Route[] Routes { get; set; }
    public Place Origin { get; set; }
    public Place Destination { get; set; }
}

public class Route
{
    public string Name { get; set; }
    public Hop[] Hops { get; set; }
}

public class Hop
{
    public Carrier Carrier { get; set; }
    public Place Departure { get; set; }
    public Place Arrival { get; set; }
    public int DurationMinutes { get; set; }
}

public class Carrier { public string Name { get; set; } public string Code { get; set; } }
public class Place { public string Name { get; set; } public double Lat { get; set; } public double Lng { get; set; } }

2. Create a configuration class

using DataNormalizer.Attributes;
using DataNormalizer.Configuration;

[NormalizeConfiguration]
public partial class SearchNormalizer : NormalizationConfig
{
    protected override void Configure(NormalizeBuilder builder)
    {
        builder.NormalizeGraph<SearchResponse>(); // discovers Route, Hop, Carrier, Place
    }
}

3. Normalize and denormalize

var result = SearchNormalizer.Normalize(searchResponse);

// Access the root directly
Console.WriteLine(result.Result.RoutesIndices.Length); // 2

// Access typed collections
Console.WriteLine(result.RouteDtos.Length);    // 2
Console.WriteLine(result.CarrierDtos.Length);  // 3 (deduplicated)
Console.WriteLine(result.PlaceDtos.Length);    // 3 (deduplicated)

// Serialize to JSON
var json = JsonSerializer.Serialize(result);

// Denormalize back to the original object graph
var restored = SearchNormalizer.Denormalize(result);

When To Use It

Good for:

  • Graph-like data where entities repeat across branches (routes, hops, places)
  • High-volume APIs where payload size and parse time matter
  • Replacing hand-written normalization or ad-hoc deduplication logic

Not ideal for:

  • Tiny payloads where overhead isn't meaningful
  • Simple flat lists with no shared references
  • Very specific legacy wire formats you can't change

Documentation

Target Frameworks

Component Targets
Runtime library net6.0, net7.0, net8.0, net9.0, net10.0
Source generator netstandard2.0 (Roslyn requirement, bundled in NuGet package)

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net7.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
0.1.4 82 4/3/2026
0.1.3 89 4/3/2026
0.1.2 100 3/25/2026
0.1.1 75 3/25/2026
0.1.0 79 3/25/2026
0.0.1 84 3/23/2026