# docs(persist): clarify createJSONStorage behavior and warn about production use

createJSONStorage is widely used in examples but lacks type safety and runtime validation — data read from storage is blindly cast to the state type, so corrupt or stale values are silently accepte…

| | |
| --- | --- |
| Status | COPILOT-WRITTEN · MERGED |
| Source | github:pmndrs/zustand#3541 |
| Original | https://github.com/pmndrs/zustand/pull/3541 |
| Workspace | Showcase |
| Tags | github, code, docs |

## What it does

**A single-file change to persist.md, 20 lines.**

Copilot opened this, so there is no author to ask. 1 file changed across 1 area of pmndrs/zustand, 18 added and 2 removed.

createJSONStorage is widely used in examples but lacks type safety and runtime validation — data read from storage is blindly cast to the state type, so corrupt or stale values are silently accepte…

**Diagram: Where the change landed. Node size is not to scale; the line counts are exact.**

- zustand (1 files, 20 lines) [accent]
- Docs (1 file, 20 lines) [neutral]
- persist.md (+18 −2) [bad]
- zustand -> Docs
- Docs -> persist.md : heaviest

- **Written by an agent.** Copilot authored this change. The usual review shortcut, asking the person who wrote it, is not available, so everything a reviewer needs has to come from the diff itself.
- **Heaviest file.** docs/reference/middlewares/persist.md, +18 −2, which is 100% of the change.
- **No test changed.** 0 source files changed with no accompanying test edit.
- **Spread.** 1 files, 20 changed lines in total.

## How it works

**The largest single edit is in persist.md.**

Quoted verbatim from the diff. This is the biggest hunk by changed lines, which is where a reviewer's attention usually belongs first.

```markdown
@@ -50,7 +50,17 @@ persist<T, U = T>(stateCreatorFn: StateCreator<T, [], []>, persistOptions: Persi
   Usually, you will return an object with the methods you want to expose.
 - `persistOptions`: An object to define storage options.
   - `name`: A unique name of the item for your store in the storage.
-  - **optional** `storage`: Defaults to `createJSONStorage(() => localStorage)`.
+  - **optional** `storage`: A `PersistStorage` object used to read and write the persisted state.
+    Defaults to `createJSONStorage(() => localStorage)`. The getter function passed to
+    `createJSONStorage` is evaluated lazily (only when the storage is first accessed), which
+    prevents errors in environments where storage APIs like `localStorage` are unavailable at
+    module evaluation time — for example, during server-side rendering (SSR).
+    > **Warning:** `createJSONStorage` relies on `JSON.parse` and `JSON.stringify` and does not
+    > perform any runtime validation. The value read from storage is cast directly to your state
+    > type without checking its shape, so corrupt, stale, or tampered data will not be caught at
+    > runtime. For production use, implement a custom `PersistStorage` that validates the
+    > deserialized value — for example with a schema validation library such as
+    > [Zod](https://zod.dev).
   - **optional** `partialize`: A function to filter state fields before persisting it.
   - **optional** `onRehydrateStorage`: A function or function returning a function that allows
     custom logic before and after state rehydration.
```

_docs/reference/middlewares/persist.md, +18 −2. The largest hunk in the largest file._

## File map

**Every file this change touches, heaviest first.**

Role is inferred from the path and risk from the path and the line count. Neither was read from the code.

| File | Role | Change | Risk |
| --- | --- | --- | --- |
| `docs/reference/middlewares/persist.md` | documentation | modified, +18 −2 | none |

## Receipts

**Where each number above comes from.**

All of it is arithmetic over the file list and the check results, so it can be recomputed from the source.

- **Claim.** Copilot wrote this change.
  - Evidence: copilot-swe-agent
  - Where: commit 877aaab
- **Claim.** 1 files changed, +18 −2.
  - Evidence: docs/reference/middlewares/persist.md (+18 −2)
  - Where: https://github.com/pmndrs/zustand/pull/3541
- **Claim.** persist.md carries 100% of the changed lines.
  - Evidence: 20 of 20 changed lines
  - Where: docs/reference/middlewares/persist.md
- **Claim.** It arrived as 3 commits.
  - Evidence: docs: expand storage option description in persist.md
  - Where: commit 877aaab
- **Claim.** CI reports 0 failing of 30.
  - Evidence: Continuous Releases
  - Where: CI check: Continuous Releases
- **Claim.** The change drew 3 comments.
  - Evidence: [vc]: #BJUt6/Z9YB4pR/pNZYsQFt/jJzCWwO78iFwt9e4G52E=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ6dXN0YW5kLWRl…
  - Where: comment by vercel[bot]

## Merge order

**A reading order for the review, not a deployment plan.**

Ordered by how expensive each part is to get wrong, which is a structural property rather than an informed one.

1. **Then persist.md.** The largest single file in the change, +18 −2.
2. **Decide whether a test is needed.** Nothing in this change adds or edits one.

## What this brief could not check

- No language model read this change. Every statement here is computed from file paths, line counts, and CI status, so it describes the shape of the diff and not what the diff means.
