Skip to content

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.

Style:
Add:

Generated code

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 null in the sample and a key that wasmissing from one element of a list both becomeT? in Dart — but only the missing one is skipped bytoJson when null, so a round-trip reproduces your input instead of inventing keys.
  • 1 and 1.0 are read from the raw token. JSON.parse would erase the difference; this tool's own reader keeps it, so a field that ever showed a decimal point becomes double, and integer fields stay int.
  • Dates are detected, strictly. A string field becomes DateTime only when every sample matches full ISO 8601 (2026-07-03,2026-07-03T10:30:00Z). One non-matching value demotes the field back to String. 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 int here and a4.5 there merge to double, and genuinely mixed types degrade to dynamic — 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 numdouble 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?

StyleBest forTrade-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_serializableTeams 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).
freezedImmutable-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.99final double price; read via (… as num).toDouble().
  • "created-at" isn't a legal identifier → the field is createdAt, but fromJson/toJson still use the original 'created-at' key.
  • "note": nulldynamic, plus a warning that the sample never showed its type.
  • "rating" is 4.5 for one user and 5 for another → merged to double.
  • "last_login" is missing from the second user → DateTime?, and toJson omits the key when it's null.
  • "users": […]List<User> with a proper User.fromJson per 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.