YAML to Dart Converter

Paste a mobile app config and read back Dart. Pick a plain class, a json_serializable model, or Freezed, and the fields, nullability and nested classes are worked out from what you pasted, reserved words and dashed keys included.

YAML to Dart conversion bench

Hand-rolled fromJson and toJson. No package beyond the Flutter SDK, no build step, every line is something you read on the spot.

YAML in0 lines
Dart outWaiting for YAML
0 classes

Codegen is a tax. Sometimes it is worth paying.

Flutter release builds strip dart:mirrors, so nothing in a shipped app reads a class at runtime and writes its JSON handling for you. Python gets that for free from duck typing. JavaScript gets it for free because an object is already a bag of keys. Dart does not, so every field on the config you paste above has to be spelled out somewhere, by you or by a generator running before the app builds.

Nothing here is free. The three tabs above are three places to pay.

A plain class costs typing and nothing else. json_serializable and Freezed cost a dependency and a build step, in exchange for less code sitting in your repository and, in Freezed's case, equality and copyWith you never have to write by hand.

What build_runner buys you

Pick either generated style and the output above references functions that do not exist yet, _$AppConfigFromJson and friends. Those come from running dart run build_runner build --delete-conflicting-outputs in the project that owns this file, which reads the annotations and writes a .g.dart file next to it, a .freezed.dart file too if you picked Freezed. Skip that step and the analyzer marks the whole file red, not because the code is wrong, but because the part it depends on has not been generated yet.

Most teams keep build_runner watch running while they work, so the generated files stay current as the model changes, and gitignore the generated output so it never causes a merge conflict. A CI pipeline then runs the build once before tests. It is one more moving part in exchange for boilerplate you stop writing by hand, and whether that trade is worth it depends on how often the shape of this file changes.

StyleExtra packagesEquality, toStringcopyWithReach for it when
Plain classNoneManual, or tick Equatable aboveYou write itA small config object, or a package you do not want a build step in
json_serializablejson_annotation, plus build_runner as a dev dependencyNot includedYou write itA class that already exists and only needs the JSON glue added
Freezedfreezed_annotation, plus freezed and json_serializable as dev dependenciesGeneratedGeneratedA model you compare often, or pass through setState and equality-sensitive widgets a lot

Field names collide with what Dart already claims

A YAML key can be almost any string. A Dart field cannot. Convention also expects camelCase, not the kebab-case that reads naturally in a config file nobody writes Dart in, so base-url becomes baseUrl before it reaches the class at all. Two harder cases show up in the sample above.

YAML in
class: premium
2fa-required: true
Plain class out
final String class_;final bool k2faRequired;factory AppConfig.fromJson(Map<String, dynamic> json) => AppConfig(class_: json['class'] as String,k2faRequired: json['2fa-required'] as bool,);

class is a reserved word in Dart, so it cannot be a field name at all, not even inside a class named something else. The trailing underscore is the smallest change that still compiles. 2fa-required breaks a different rule, since no Dart identifier may open with a digit, so the field picks up a k prefix instead. In the plain class style neither rename costs you anything at the call site, because json['class'] still points at the original key. Switch to json_serializable or Freezed and the same rename needs an explicit @JsonKey(name: 'class') above the field, or the generated code would look for a key spelled class_ in the response and never find it.

Why doesn't my release date show up as a DateTime?

Because nothing in a Map<String, dynamic> knows what a date is. json.decode and the YAML reader above both produce strings, numbers, booleans, lists, maps and null, full stop. An unquoted 2026-08-30 in the YAML file is recognised as a date while this tool is reading the file, but the moment it becomes a field on a JSON-shaped model, it is a plain String unless something calls DateTime.parse on the way in.

Tick Parse ISO dates as DateTime and that call gets written for you, matched with .toIso8601String() on the way back out. Leave it off and the field stays String, which is the honest default for a value most services hand you as text anyway.

DateTime.parse reads a string with no timezone offset as local time, not UTC. A field written 2026-08-30T09:00:00 without a trailing Z parses to 9 AM wherever the device happens to be, which is rarely what a server meant. Append the offset in the source file, or call .toUtc() yourself once the value is parsed, if the config is meant to carry an absolute instant.

Lists that disagree with each other

The three entries under feature-flags in the sample do not all carry the same keys. Two have rollout, one does not. Rather than reading only the first entry and guessing, the generator folds every entry into one shape: a key present everywhere stays required, a key present in some entries but not others comes through nullable, and the notes panel below the editors names which key and how many entries were short.

An empty list carries no evidence about what it holds, so it types as List<dynamic> until a populated sample says otherwise. A list mixing a real value with a bare null also falls to dynamic, on purpose rather than by omission: whether that null means "not applicable" or "not loaded yet" depends on the app, and typing it either way here would be a guess dressed up as an answer.

Where this reader draws the line

A JSON response fits the JSON to Dart converter better than this one, since it infers from response shapes instead of a config file. A YAML file that already has a schema is worth running through the YAML to JSON Schema converter before anything gets typed by hand. And a file that will not parse here at all deserves a pass through the YAML validator first, since it lists every problem at once instead of stopping at the first.

Questions about generating Dart from YAML

build_runner errors, reserved keys, dates that stayed strings, and why a field turned out nullable.

Should I pick json_serializable or Freezed?

It comes down to whether you compare these objects often. json_serializable only writes fromJson and toJson, so two configs with identical fields are still != to each other unless you add equality yourself. Freezed generates equality, toString and copyWith alongside the JSON methods, at the cost of a second generator running in the same build_runner pass. For a config you read once at startup and never compare, json_serializable is the smaller dependency. For a model you pass through state management or compare in tests, Freezed usually pays for itself.

build_runner failed with "Target of URI hasn't been generated". What now?

That error means the analyzer found the part directive, for example part 'app_config.g.dart', before that file existed. Run dart run build_runner build --delete-conflicting-outputs in the project the class lives in, and the missing file gets written. It reappears any time you change a field and forget to rerun the build, which is the usual reason to leave build_runner watch running while you work instead of triggering it by hand each time.

Why is my class key now class_?

Because class is a reserved word in Dart and cannot be used as a field name under any circumstances, not just inside a class of the same name. The trailing underscore is the smallest rename that still compiles. In the plain class style this costs nothing, since json['class'] still reads the original key. In json_serializable and Freezed, the rename needs an explicit &#64;JsonKey(name: 'class') so the generated code looks for the right key in the response.

Two of three feature flags have rollout. Why is the field nullable instead of required?

The generator folds every entry in a list into one shape before writing a class. A field missing from even one entry becomes nullable, since a required field that is sometimes absent would throw the moment fromJson hit the entry that lacks it. Untick "Missing-in-some fields are nullable" above and the field stays required instead, with a warning in the notes panel that a future response omitting it will crash rather than parse.

Does the Equatable option work with json_serializable too, not just the plain class?

Yes. json_serializable only generates fromJson and toJson, nothing about equality, so ticking Equatable is often the more useful combination of the two toggles: JSON handling from the generator, value equality from the package. It is hidden under the Freezed tab because Freezed already writes equality itself, and mixing the two would fight over which one owns ==.

Why is my release date a String instead of a DateTime?

Because that is the default, and it matches what a Map<String, dynamic> contains: JSON has no date type, so an ISO date is text until something parses it. Tick "Parse ISO dates as DateTime" and every date or datetime field gets DateTime.parse written into fromJson and toIso8601String() written into toJson. Leave it unticked and the field stays text, which is also the safer default if you are not certain the value always carries a full timestamp.

I pasted a file with a --- separator and anchors. What happened to them?

The --- is read as ordinary text rather than a document boundary, so the file is treated as one continuous block, and an anchor like &defaults or an alias like *defaults is left as unrecognised text rather than resolved into the value it points at. Both are more common in deployment manifests than in the app-config files this converter is built around. A file that leans on either reads correctly through the YAML to Go or YAML to Python converter instead, which do resolve them.

Is my config file uploaded anywhere?

No. Reading the YAML and writing the Dart both happen in JavaScript inside this page, after it has finished loading. Nothing is sent anywhere, so a file holding an API base URL, a session length or anything else you would rather not paste into a public form stays on your machine. Closing the tab clears both panes, and nothing is kept between visits.