Skip to main content

JSON to Dart Classes for Flutter: fromJson, toJson, and Null Safety Without the Boilerplate

Turn any JSON payload into Flutter-ready Dart model classes with fromJson factories, toJson, null-safe nullable types, and nested objects handled for you.

Published By Li Lei
#flutter #dart #json #code-generation

JSON to Dart Classes for Flutter: fromJson, toJson, and Null Safety Without the Boilerplate

Every Flutter developer has done it: an endpoint returns a chunk of JSON, and you sit down to hand-write a Dart model. You declare the fields, add a constructor, write fromJson, write toJson, sprinkle in as String casts, then realize the response has a nested address object that needs its own class. Forty lines later you have one model, and the backend has six more endpoints.

That manual loop is where bugs hide. A typo in a map key reads null forever. A number you assumed was an int arrives as 4.5 and crashes a user's session. A field that's present in some array elements but missing in others throws at parse time because you forgot to null-guard it. The JSON to Dart Class tool exists to delete that whole class of work — paste JSON, get model classes that compile.

What the tool actually generates

For every JSON object, the tool produces a Dart class with final typed fields, a named constructor, a factory ClassName.fromJson(Map<String, dynamic> json), and a Map<String, dynamic> toJson(). The concrete win is that it writes the fromJson factory for you, including the right cast for each field and the recursion into nested classes — the part everyone hand-types and occasionally gets wrong.

Type inference is the part that saves you from runtime surprises. JSON has a single number type; Dart has int and double. A whole-number sample infers int, a fractional sample infers double, and when a field is 0 in one object but 4.5 in another, the union widens to num so both literals fit without a cast. A key that's only ever null becomes dynamic rather than guessing.

Key naming is handled too. JSON keys like site_admin and created-at become valid camelCase Dart identifiers (siteAdmin, createdAt), while fromJson and toJson keep reading and writing the original wire key. The model round-trips against the server without losing a field, and your widget tree reads clean Dart names.

A worked example

Here's a small payload — a user with a nested address and a list of tags:

{
  "id": 42,
  "user_name": "li.lei",
  "is_active": true,
  "balance": 0,
  "address": {
    "city": "Shanghai",
    "zip": "200000"
  },
  "tags": ["dev", "flutter"]
}

With the root class named User, the tool produces something like:

class User {
  final int id;
  final String userName;
  final bool isActive;
  final num balance;
  final UserAddress address;
  final List<String> tags;

  User({
    required this.id,
    required this.userName,
    required this.isActive,
    required this.balance,
    required this.address,
    required this.tags,
  });

  factory User.fromJson(Map<String, dynamic> json) => User(
        id: json['id'] as int,
        userName: json['user_name'] as String,
        isActive: json['is_active'] as bool,
        balance: json['balance'] as num,
        address: UserAddress.fromJson(json['address'] as Map<String, dynamic>),
        tags: List<String>.from(json['tags'] as List),
      );

  Map<String, dynamic> toJson() => {
        'id': id,
        'user_name': userName,
        'is_active': isActive,
        'balance': balance,
        'address': address.toJson(),
        'tags': tags,
      };
}

A second UserAddress class is emitted alongside it. Notice balance came out as num, not int, because the value 0 is whole but a balance can carry decimals — and notice user_name stays as the wire key inside fromJson while the Dart field is userName. Both of those are the kind of detail that takes a careful human a minute each and the tool gets right every time.

Null safety, the right way

Sound null safety is where hand-written models go wrong most often, because the rules depend on data you haven't fully looked at. The tool reads every sampled object and decides per field. A field that appears in every object and is never null is non-nullable, and its constructor parameter is required — you can't build the object without it. A field missing from some array element, or ever seen as null, becomes a nullable T? and drops the required keyword, so construction stays legal.

The generated fromJson matches that decision with a guard:

avatar: json['avatar'] == null ? null : json['avatar'] as String,

This is exactly what you want for a Firebase document where not every record carries every key, or a REST body where optional fields come and go. If you're targeting a legacy pre-2.12 project, you can turn null safety off entirely and every parameter becomes a plain optional.

Nested objects, lists, and json_serializable

Real payloads nest. The tool lifts each nested object into its own named class — User.address becomes UserAddress — and the parent's fromJson recurses into UserAddress.fromJson(...). Arrays of objects fold into one shared class instead of N near-duplicate models, so a list of 200 items still gives you exactly one element type to reason about; any key present in only some elements is treated as nullable. Lists of scalars become List<T>.from(...).

If your project already runs build_runner, flip on the json_serializable option. Instead of an inline body, the tool emits @JsonSerializable(), the part 'user.g.dart'; directive, @JsonKey(name: 'created_at') for every renamed field, and the _$UserFromJson / _$UserToJson bridges. Drop the file in, run dart run build_runner build, and the generated code follows the conventions your codebase already uses — just remember to add json_annotation, json_serializable, and build_runner to pubspec.yaml, since those references won't compile without the generated .g.dart file.

How I actually use it

I keep this tool open in a tab while I'm wiring up a new API. My habit is to paste a real response straight from the network panel — not a trimmed-down example — because the tool's inference is only as good as the sample, and a real response shows me which fields are genuinely optional. When the JSON looks messy I first run it through the JSON formatter to confirm it parses and to eyeball the shape, then drop the clean version into the Dart generator. The two together turn "I got a new endpoint" into a thirty-second job: format, generate, copy, paste. The part I appreciate most is that the nested classes come out already named and wired, so I'm not the one deciding whether profile.settings.notifications deserves its own class at 6pm on a Friday.

A couple of things to double-check

The tool is faithful to your sample, which means a field that's only ever whole in your one example will infer as int even if production sometimes sends a decimal — widen it to double or num by hand if you know that's possible. And when you rename a key for readability, keep the original wire key inside fromJson/toJson (or @JsonKey(name:) in codegen mode); changing the map key to the camelCase name makes deserialization hunt for a key the server never sends. Everything runs in the browser, so the JSON you paste never leaves the tab — copy the generated Dart for sensitive payloads instead of sharing the URL.


Made by Toolora · Updated 2026-06-13