DevKitHub

Programming

JSON to Dart Model Class Generator for Flutter

Paste a JSON response to get Dart classes with final fields, a const constructor and fromJson and toJson, or json_serializable annotations. Every element of an array is merged into one class, so fields that are sometimes null or missing come out nullable.

14 lines
90 lines

Decode

Root type
Root
Decode
Root.fromJson(jsonDecode(text) as Map<String, dynamic>)
File
root.dart
  • $.due_at: every sample is an RFC 3339 date-time, so the type is DateTime. DateTime.parse fails on anything else, including "", and toIso8601String() writes the same instant in UTC, so +05:30 comes back as Z.

The classes describe the sample you paste: paste an array of several responses so that fields which are sometimes missing or null come out nullable. Decode needs import 'dart:convert'; json_serializable needs build_runner to write the .g.dart file.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

Every sample is merged position by position before any Dart is written, so a list of tasks gives one class typed from all of them, and sound null safety states exactly what the samples showed. A key with a value in every sample is a final, non-nullable field and a required named parameter of the const constructor. One that is null in some sample is nullable, String?, but still required, because the API does send the key; one missing from some objects is nullable and optional, and toJson leaves it out while it is null, so a key that was missing stays missing. A cast is where a model breaks when the API changes: a null or a missing key for a non-nullable field fails with "type 'Null' is not a subtype of type 'String' in type cast".

Numbers are the classic trap: jsonDecode gives an int for 5 and a double for 5.0, and each fails a cast to the other: json['price'] as double throws "type 'int' is not a subtype of type 'double'" the first time a price is round. A field is int only when every sample is written as an integer, and otherwise double, read as (json['price'] as num).toDouble(), which accepts both. On the Dart VM an int holds 64 bits and a larger integer is read as a rounded double; on the web, compiled to JavaScript, an int is a JavaScript number, so integers beyond 2^53 lose digits, with a warning for each. Lists need care too: a decoded list is a List<dynamic> whatever it holds, so json['tags'] as List<String> fails even when every element is a string. So each list is read element by element, (json['tags'] as List<dynamic>).map((e) => e as String).toList(), and a list of objects calls the element class’s fromJson.

A string field is DateTime, when that option is on, only when every sample is an RFC 3339 date-time with a zone and at most six fraction digits. DateTime.parse accepts far more, including 2026-02-30, which it moves into March, and keeps only microseconds; toIso8601String() writes the same instant back in UTC, so +05:30 returns as Z. A date, or a time without a zone, stays String, as DateTime would read it as local time. Field names are lowerCamelCase in ASCII, the only letters Dart identifiers allow, so ünï gives uni, and a reserved word such as class or default, or a name the class already has, such as hashCode, toJson or int, gets Value added. With json_serializable each class has @JsonSerializable(), @JsonKey(name: …) only where the key differs from the field, @JsonKey(includeIfNull: false) on optional fields, and explicitToJson: true when it holds another model, so toJson returns nested maps rather than objects.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

Object keys must be strings in double quotes, as in {"name": 1}.

{id: 1, name: Ada, tags: [a, b]}
Why:
The output of print(json), or of a Map’s toString(), which Dart writes as {id: 1, name: Ada} without quotes. The Flutter debug console shows decoded maps this way, so they are easy to copy by mistake.
Fix:
Print jsonEncode(json) instead, or copy the raw response body from the network view in Flutter DevTools.

Trailing comma: JSON does not allow a comma after the last item of an array.

{"id": 1, "tags": ["a",],}
Why:
Trailing commas, which Dart allows after the last element of any list, map or argument list and which dart format adds itself. jsonDecode refuses them too, with "Unexpected character".
Fix:
Remove the comma after the last element and after the last key.

type 'int' is not a subtype of type 'double' in type cast

Why:
A model that reads json['price'] as double. jsonDecode gives an int for a round number such as 5, and the cast fails; the reverse, json['count'] as int, fails on 5.0.
Fix:
Read doubles as (json['price'] as num).toDouble(), as the generated code does, and include a sample with a fraction for any field that can have one.

type 'List<dynamic>' is not a subtype of type 'List<String>' in type cast

Why:
json['tags'] as List<String>. jsonDecode builds a List<dynamic> whatever the elements are, and a cast checks the list’s type rather than converting it.
Fix:
Map each element, (json['tags'] as List<dynamic>).map((e) => e as String).toList(), or copy it with List<String>.from(json['tags']).

Target of URI hasn't been generated: 'package:app/root.g.dart'.

Why:
The json_serializable output refers to a part file that does not exist yet. build_runner writes it, and the part directive must match the name the code is saved under.
Fix:
Save the code under the file name shown, add json_annotation, json_serializable and build_runner to pubspec.yaml, and run dart run build_runner build.

Frequently asked questions

Should I write fromJson by hand or use json_serializable?
Hand-written code needs no packages and no build step, and you can read exactly what it does. json_serializable generates the same kind of code from annotations, which is easier to keep right as models grow, at the cost of three packages and running build_runner after each change. Both write the same JSON.
How do I decode a JSON array into a list of models?
(jsonDecode(text) as List<dynamic>).map((e) => Task.fromJson(e as Map<String, dynamic>)).toList(), with jsonDecode from dart:convert. The tool shows the exact expression for your JSON under Decode.
Why is a field that looks whole typed double?
Because some sample has a fraction, or is written with a decimal point, such as 1.0 or 0.5. JSON has one kind of number, but Dart has two, and a field read through num accepts both.
Which Dart version does the output need?
The hand-written classes need Dart 2.12 or later, for sound null safety, and have no Flutter imports, so they work in any Flutter or Dart project. With json_serializable, the code build_runner writes depends on its version: json_serializable 6.14 writes code that needs Dart 3.8.

Last updated