Tools
JSON to Dart
Paste JSON, get null-safe Dart model classes withfromJson and toJson — plain,json_serializable or freezed, withcopyWith and value equality on request. Runs entirely in your browser.
Generated code
More on this site: all tools ·JSON formatter ·regex checker ·the arcade
Beta — the generated code is unit- and compile-tested, but inference from one sample can't be perfect; on rare occasions the output may need a tweak. Spot something wrong?Tell me.
What is this tool?
A free JSON-to-Dart converter. Paste a JSON document — an API response, a config file, a Firestore export — and it generates idiomatic, null-safe Dart classes ready to drop into a Flutter or server-side Dart project: finalfields, a const constructor withrequired parameters, a fromJson factory and a toJson method that round-trips your original keys exactly. Output regenerates live as you type.
Your JSON never leaves the browser. Parsing, inference and code generation are pure client-side code — the page works offline once loaded. That matters, because the JSON people convert is usually a real response from a private API.
How to use it
- Paste JSON on the left — the Dart appears on the right as you type.
- Name the root class (defaults to
Response); nested classes are named from their keys, singularized inside lists (users: […]→User). - Pick a style: plain classes (zero dependencies), json_serializable annotations, or freezed for immutable models with unions-ready syntax.
- Tick what else you want generated — copyWith,
==&hashCode,toString. freezed already writes all three, so the toggles go inert when it's selected. - Check the warnings panel: every place your sample was too thin to type (an empty array, an always-null field) is called out by its JSON path instead of guessed silently.
- Copy or Download the file, named after the root class (
response.dart).
What the inference gets right
- Nullable vs optional are different things. A key whose value was
nullin the sample and a key that wasmissing from one element of a list both becomeT?in Dart — but only the missing one is skipped bytoJsonwhen null, so a round-trip reproduces your input instead of inventing keys. 1and1.0are read from the raw token.JSON.parsewould erase the difference; this tool's own reader keeps it, so a field that ever showed a decimal point becomesdouble, and integer fields stayint.- Dates are detected, strictly. A string field becomes
DateTimeonly when every sample matches full ISO 8601 (2026-07-03,2026-07-03T10:30:00Z). One non-matching value demotes the field back toString. Toggle it off if you'd rather keep strings. - List elements are merged, not sampled. Every element of an array contributes: a field missing from one object becomes optional, an
inthere and a4.5there merge todouble, and genuinely mixed types degrade todynamic— with a warning naming the exact path. - Identical shapes become one class. Two objects with the same fields collapse into a single class (toggle it off if you expect them to diverge later).
freezed models, straight from JSON
Pick freezed and each class comes out as an immutable freezed model — a const factory whose parameters are your fields, the _$Model mixin, and afromJson redirect — so copyWith, value equality, toString and toJson all arrive from build_runner rather than from hand-written boilerplate.
@freezed
abstract class User with _$User {
@JsonSerializable(explicitToJson: true)
const factory User({
required int id,
required String name,
@JsonKey(name: 'last_login', includeIfNull: false) DateTime? lastLogin,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}Two details that bite people, both handled here. The class is declared abstract, which freezed 3.0 made mandatory — its changelog puts it plainly: "Freezed classes should now either be abstract,sealed, or manually implement_$MyClass" (sealed is for unions, which a single JSON sample never produces). And@JsonSerializable(explicitToJson: true) sits on the factory, not the class: without it a nested model serializes as the Dart object instead of a map, andjsonEncode throws at runtime on output that looked fine in the editor.
On freezed 2.x, delete the abstract keyword — nothing else changes.
A copyWith that can actually set null
Tick copyWith on the plain or json_serializable styles and you get the method most generators write:
User copyWith({String? name}) => User(name: name ?? this.name);…except that version has a hole. For a nullable field,?? cannot tell "leave this alone" apart from "set it back to null" — user.copyWith(lastLogin: null)silently does nothing, which is a genuinely nasty bug to chase. So nullable fields get a private sentinel instead:
const Object _unchanged = Object();
User copyWith({
String? name,
Object? lastLogin = _unchanged,
}) {
return User(
name: name ?? this.name,
lastLogin: identical(lastLogin, _unchanged) ? this.lastLogin : lastLogin as DateTime?,
);
}Non-nullable fields keep the short ?? form — the sentinel only appears where it earns its keep. It is the same distinction freezed makes internally.
Value equality, deep where it counts
Tick == & hashCodeand models compare by value, so two responses parsed from the same bytes are equal and behave in a Set or as aMap key. The catch generators usually miss:Dart's == on a List is identity, so a naive tags == other.tags makes two structurally identical models compare unequal. List fields go through a small generated deep-equality helper instead, with a matching _listHash so equal objects really do hash alike. Still zero dependencies — no package:collection, no Equatable.
Null safety done properly
Since Dart 2.12, types are non-nullable by default — and generated code should embrace that, not fight it. The classes here make every field the sample supports non-nullable andrequired; only fields that were null or missing getT?. There is no late, no!, and no nullable-everything cop-out: if your sample shows the data is always there, your code shouldn't have to null-check it forever. If you don't trust your sample, flip on All fields optional and every field becomes T? honestly.
The num → double pitfall
The #1 runtime crash from naive converters: a field typeddouble, cast as json['price'] as double. The moment the API returns a whole number — 5 instead of 4.5 — Dart decodes it as int, the cast throws, and the crash lands in production. Generated code here always reads double fields as:
price: (json['price'] as num).toDouble(),which accepts both. Integer fields keep the strictas int cast — if the API sends 1.5 where your sample said 1, that should fail loudly rather than silently truncate.
Plain classes, json_serializable or freezed?
| Style | Best for | Trade-off |
|---|---|---|
| Plain classes (default) | Most projects — zero dependencies, no codegen step, the whole class is readable in one screen. copyWith, == and toString are opt-in checkboxes. | Serialization logic lives in your source; edits to the shape mean editing two methods. |
| json_serializable | Teams already running build_runner — annotations only, the generated .g.dart part does the work. | Adds json_annotation + a build step; explicitToJson: true is included so nested models actually serialize (a classic gotcha). |
| freezed | Immutable-model codebases — copyWith, value equality and toString come from the generator, and the syntax extends to unions later. | Two packages plus build_runner, and two part files per model. Overkill if you only wanted fromJson. |
Worked example
Load the Sample and look at what comes back:
"price": 9.99→final double price;read via(… as num).toDouble()."created-at"isn't a legal identifier → the field iscreatedAt, butfromJson/toJsonstill use the original'created-at'key."note": null→dynamic, plus a warning that the sample never showed its type."rating"is4.5for one user and5for another → merged todouble."last_login"is missing from the second user →DateTime?, andtoJsonomits the key when it's null."users": […]→List<User>with a properUser.fromJsonper element.
FAQ
Is it free? Yes — free, no sign-up, no limits.
Is my JSON uploaded anywhere? No. Everything runs in your browser; the page works offline after loading. Your last input is kept in your own browser's local storage so it survives a reload — nothing else.
Why did a field come out dynamic?The sample didn't pin it down: the value was alwaysnull, the array was empty, or two samples disagreed (a string here, a number there). The warnings panel names the exact JSON path. Add one representative value and regenerate.
Why is a field nullable when my API always sends it?Because in this sample it was null or missing at least once. The tool can only be as sure as your sample — paste a richer one, or edit the generated field.
Can it read a JSON Schema or OpenAPI spec? Not yet — v1 infers from JSON samples. Schema input is planned.
Does it generate freezed classes? Yes — pickfreezed under Style. The output is freezed 3.x syntax (abstract class + const factory); on freezed 2.x, drop the abstract keyword and nothing else changes.
Why are the copyWith and == checkboxes greyed out? Because freezed writes those itself, into the.freezed.dart part. Generating them here too would just collide with it. Switch back to plain or json_serializable and the toggles come alive.
Which Dart does the output target? Null-safe Dart — 2.12+ and Dart 3, with no dependencies at all in plain-class style. Two caveats, both flagged in the output: the==/hashCode option usesObject.hash, which is 2.14+, and when your sample has a missing key the annotation styles emitincludeIfNull: false, which current json_serializable compiles to null-aware elements — that part needs SDK 3.8+.
Built from scratch for this site in vanilla TypeScript — a pure, unit-tested inference engine (token-level number reading, a proper merge lattice, golden-tested emitters) and zero runtime dependencies.