Normalizr guide: normalize nested JSON for Redux state
Contents
Nested JSON responses look convenient until you try to update one entity buried three levels deep in a Redux store, then every reducer touching that tree becomes a liability. Normalizr solves this by flattening arbitrarily nested API payloads into flat, ID-keyed entity maps, so updates hit one place instead of cascading through duplicated copies.
This guide covers installing Normalizr and json-api-normalizer, writing schema.Entity definitions, running normalize and denormalize, and deciding whether to migrate to @data-client/normalizr.
Normalizr in short: What it does and when to use it
Nested API responses turn a simple Redux update into a full-tree rewrite: change one comment on a post and every reducer touching that post has to re-render. Normalizr fixes this by flattening JSON into a flat entity map, keyed by ID, so a single record update touches one branch of state.
The normalize function takes raw data and a schema.Entity definition and returns entities plus a result of IDs. Redux normalized state built this way makes selectors cheaper and update logic predictable, precisely by cutting this kind of redundant re-render. This guide is tested against normalizr v3.6.2 and @data-client/normalizr v0.15.x.
Normalizr's GitHub repository was archived (read-only) in March 2022, which matters when you're choosing it over newer alternatives: the API is stable, but nothing further is coming from the original project.
This piece covers schema design, denormalize, JSON API Normalizer's field mapping, and when Redux Toolkit's createEntityAdapter is the better call.
What problem does normalizr solve? Nested JSON before and after
A nested JSON API response forces every reducer that touches an object to re-render when any child changes: update one comment on a blog post and the entire post tree, author included, gets rewritten in Redux. Normalizr solves this by producing a flat entity map, keyed by ID, from a schema.Entity definition and the normalize function.
Here's the before:
{
"id": "1",
"author": { "id": "u1", "name": "Kate" },
"comments": [
{ "id": "c1", "text": "Nice article", "author": { "id": "u2", "name": "Tom" } }
]
}
After running it through normalize(data, articleSchema), where articleSchema is a schema.Entity wired to nested schema.Entity definitions for users and comments, the result is a Redux normalized state shape:
const entities = {
articles: { "1": { id: "1", author: "u1", comments: ["c1"] } },
users: { "u1": { id: "u1", name: "Kate" }, "u2": { id: "u2", name: "Tom" } },
comments: { "c1": { id: "c1", text: "Nice article", author: "u2" } }
};
Each entity lives in its own table, which you can reload refresh session to pull the latest data. Updating Tom's name touches entities.users.u2 only; denormalize reassembles the nested view for rendering when needed.
Normalizr's GitHub repository was archived (read-only) in March 2022, yet the library still sits at version 3.6.x and holds up fine in existing production apps.
Installing normalizr and json-API-normalizer
Install Normalizr and JSON API Normalizer from npm or yarn before touching any reducer code:
npm install normalizr json-api-normalizer
# Or
yarn add normalizr json-api-normalizer
This walkthrough is tested against normalizr v3.6.2 and json-api-normalizer v1.0.4, the current versions on the npm registry. The paularmstrong/normalizr repository was archived (read-only) in March 2022: no maintainer, no further commits, whatever's published is what you get. @data-client/normalizr has emerged as the actively developed community continuation. If you're starting a greenfield store today, install @data-client/normalizr instead and skip the migration later.
After installing, restart your dev server. If a stale bundle is cached, a tab reload usually clears it before you touch any schema file. json-api-normalizer expects a JSON:API-shaped response out of the box, mapping data, included, and relationship objects into entities without a manual schema definition, unlike normalizr's schema.Entity approach.
Normalizr npm install: Quick reference
Run npm install or yarn add once and both packages land in package.json, no extra config file needed:
npm install normalizr json-api-normalizer
# Or
yarn add normalizr json-api-normalizer
Skip global installs, Normalizr stays a per-project dependency. After install, commit the lockfile to git, then reload the tab or open another window so the dev server picks up the new schema.Entity import before you touch normalize or denormalize calls. Check the inline comments in the Normalizr GitHub repo for a working const setup if the entities don't resolve.
How normalize works: schema.Entity by example
The normalize function takes a nested API response and flattens it into a lookup table of entities, keyed by ID, based on the schema.Entity definitions you pass in. Everything downstream, your Redux store, your selectors, reads from that flat table instead of walking nested JSON.
Schema.Entity describes one resource type that deserves its own table: a user, an article, a comment. schema.Object is for plain nested structures that don't need normalizing on their own, like an address block sitting inside a user record.
Here's a comments-under-articles example using idAttribute and processStrategy, tested against normalizr v3.6.x:
import { schema, normalize } from 'normalizr';
const comment = new schema.Entity(
'comments',
{},
{
idAttribute: 'commentId',
processStrategy: (value) => ({...value, createdAt: new Date(value.createdAt) }),
}
);
const article = new schema.Entity('articles', { comments: [comment] });
const normalized = normalize(apiResponse, article);
IdAttribute matters the moment your API doesn't use id as the primary key, GitHub-style APIs, for instance, often ship commentId or nodeId fields instead. processStrategy runs before normalization, so it's the right place to coerce date strings or strip fields you don't want cached.
The output is { entities, result }, entities.comments, entities.articles, and a result that's just the top-level ID. denormalize reverses it when a component needs the full nested shape back. JSON:API's 1.1 specification formalizes this same entity-and-relationship split at the wire level, which is why schema.Entity maps onto JSON:API resources with almost no translation work.
How to denormalize data in JavaScript with denormalize
The denormalize function reverses normalize: give it an ID, the schema.Entity that describes it, and the full entities table, and it walks the schema graph to rebuild the nested shape your view components expect.
import { denormalize } from 'normalizr';
const article = denormalize(articleId, articleSchema, state.entities);
// { id: 1, title: '...', author: { id: 5, name: '...' }, comments: [{...}, {...}] }
This matters because your Redux normalized state stores author and comments as bare IDs, not objects. A selector calling denormalize reconstructs the tree on read, so a <Comments> component still receives an array of comment objects instead of an array of numbers. As of normalizr v3.6.2, per the official GitHub readme, denormalize accepts partial entity data too, returning undefined for any referenced ID missing from the table rather than throwing.
Keeping the flat entity map as the single source of truth and pushing every nested-view need through denormalize selectors cuts redundant re-renders compared to storing duplicated nested copies per view. Skip this step and hand-roll denormalization instead, and you'll end up re-implementing the same relational lookup logic schema.Entity already handles.
schema.Array vs schema.Union: Choosing the right schema type
Schema.Array fits a homogeneous list, every item is the same entity type, like a feed of article objects. schema.Union fits a heterogeneous list, comments, mentions, and reactions mixed in one array, each needing its own schema.Entity and its own denormalize shape.
According to the Normalizr GitHub repo, the library ships four composite schema types: Array, Object, Union, and Values, and picking the wrong one is the most common normalizr mistake we see in Redux codebases.
| Schema | Use when | Example |
|---|---|---|
| schema.Array | Every item is the same entity | [schema.Array(articleSchema)] for a list of articles |
| schema.Union | Items are different entity types, discriminated by a key | An activity feed mixing post, comment, and like entities |
| schema.Object | A fixed-shape nested object, not a keyed collection | A single article's metadata: { seo, analytics } block |
| schema.Values | A dictionary/map of unknown keys, all pointing to one entity type | { [userId]: commentEntity } from a JSON API Normalizr relationship map |
Schema.Union requires a schemaAttribute to tell normalizr which entity each item is, miss it and normalize silently drops the type key. This is a common way to break comment threads in production Redux stores where comments and replies share one array but not one shape.
If your data is a REST list endpoint, reach for schema.Array. If it's a polymorphic feed, schema.Union is the only correct choice, Object and Values solve a different problem entirely, not an array-shape one.
Json-API-normalizer: Handling JSON:API responses
JSON:API responses nest relationships under data, included, and relationships keys, which don't map cleanly onto Normalizr's schema.Entity structure without custom idAttribute and processStrategy wiring. JSON API Normalizer solves this by reading the JSON:API specification shape directly and flattening it into entity maps without writing schema definitions by hand.
Install it alongside normalizr with npm install json-api-normalizer and feed it a raw JSON:API document:
import normalize from 'json-api-normalizer';
const { article, comments } = normalize(apiResponse);
The library walks relationships and resolves each linked resource into its own keyed entity, matching the type field from JSON:API to the entity name, so article and comments land as separate normalized collections ready for a Redux store.
Swapping a hand-rolled schema.Entity tree for json-api-normalizer once a backend moves to strict JSON:API compliance is a common refactor, and it typically removes most of the relationship-mapping code that would otherwise live in your reducers.
On choosing between tools: createEntityAdapter from Redux Toolkit is the better fit if data never arrives in JSON:API format and you're already inside Redux Toolkit's boilerplate. Reach for json-api-normalizer, or plain normalizr with custom schemas, when the API contract is JSON:API-shaped and normalization needs to happen before data hits the store at all.
Using normalized data with Redux (and ImmutableJS)
Redux normalized state pairs with normalize and denormalize to keep component re-renders tied to the entity that actually changed, not the whole response tree. Store a schema.Entity map keyed by ID, and a React component subscribed to comments[42] only re-renders when comment 42 changes, not when a sibling post updates.
Replacing a deeply nested API response with a normalizr-normalized entity map in the Redux store cuts redundant re-renders tied to unrelated list updates. The fix is structural: nested comments arrays inside posts objects mean any comment edit invalidates every post selector downstream, until the data is flattened.
ImmutableJS complicates this pattern rather than simplifying it. Normalizr's normalize output is plain JS objects and arrays, so teams running ImmutableJS typically convert the normalized result with fromJS after normalizing, never before, since schema.Entity processing expects mutable-looking plain objects during the merge step.
On the decision between normalizr and Redux Toolkit's createEntityAdapter, the rule of thumb: reach for createEntityAdapter when entities arrive already flat and you just need sorted ID arrays and CRUD reducers. Reach for normalizr when the API response itself is nested and relational, and you need schema.Array and nested schema.Entity definitions to flatten it before it ever reaches the reducer.
They solve different halves of the same problem, and combining both, normalizr for ingestion, createEntityAdapter for reducer ergonomics, is common in production Redux stores.
Is normalizr still maintained? @data-client/normalizr migration
Normalizr (the paularmstrong/normalizr package on GitHub) is not maintained: the repository was archived (read-only) in March 2022, meaning no further commits, patches, or issue responses are coming from the original project at all. The published v3.6.2 is a stable, frozen snapshot, not a security-patched release track. @data-client/normalizr keeps the same schema.Entity, normalize, and denormalize signatures, compatible enough that swapping the import is typically a low-risk change, not a rewrite.
@data-client/normalizr is the actively developed fork, published under the data-client npm scope, adding TypeScript-first schema definitions and tighter integration with data-client's own fetch layer. If your app already leans on data-client for caching, migrate. If normalizr is a small, isolated utility feeding a Redux store via createEntityAdapter, staying on v3.6.x is defensible, the API hasn't broken in years.
A low-risk way to validate the migration: install @data-client/normalizr alongside the existing normalizr install on a separate git branch, then compare output side by side against the same fixture data. If the entity maps and denormalized output match exactly, the swap is just the import statement and the schema file, no reducer changes needed.
| Situation | Recommendation |
|---|---|
| New project, no data-client elsewhere | Evaluate @data-client/normalizr first |
| Existing normalizr + Redux Toolkit stack | Stay on v3.6.x |
| Already using data-client for fetching | Migrate for one schema definition surface |
Either package still requires createEntityAdapter or a hand-rolled reducer to consume the entities object, normalizing data solves shape, not storage.
Alternatives to normalizr: createEntityAdapter and others
Redux Toolkit's createEntityAdapter is the default alternative once you're already inside a Redux Toolkit store. It generates the same sorted-ID-plus-lookup-table shape that a hand-rolled Normalizr schema.Entity produces, without a separate normalize/denormalize dependency. If your Redux normalized state only needs flat, single-collection entities, createEntityAdapter removes a package; if your API returns deeply nested, relational payloads with polymorphic references, Normalizr's schema.Array and schema.Union still do more work per line of code.
| Option | Best for | Cost |
|---|---|---|
| Normalizr | Complex nested/relational schema, frozen API | Zero license, maintenance risk |
| createEntityAdapter | Redux Toolkit projects, flat entities | Zero license, less flexible schema |
| @data-client/normalizr | New builds wanting active development | Learning curve, newer community |
| Internal build | Highly specific denormalize logic | Engineering time, ongoing upkeep |
Some teams treat this as a build-vs-buy question and bring in outside frontend engineers to scope whether a custom normalization layer pays back faster than adopting a new dependency. Check the GitHub repo and its issue comments before deciding either way.
FAQ: Normalizr maintenance, contributing, and replacements
Should I still use normalizr in 2026?
How do I contribute to normalizr?
@data-client/normalizr instead, which is the actively developed continuation and does accept contributions.
What replaces normalizr?
Is normalizr still maintained?
@data-client/normalizr continues the project as an actively developed community fork, shipping regular releases. If you install normalizr today, plan a migration path rather than long-term reliance.
