← Back to list

Flutter: How to (de)serialize JSON Properly

A definitive guide to clean model generation that AI agents keep getting wrong.

Yuri Novicow in Easy Flutter · 2026-03-31 11:40 · 44 claps · 3.0 min read paywalled
#flutter #flutter-app-development #dartlang #flutterjson
Open on Medium ↗
Wiki topics: AGT · AI Agents CRY · Crypto & Web3 📱 · Mobile Development

Flutter: How to (de)serialize JSON Properly

A definitive guide to clean model generation that AI agents keep getting wrong.

If you are a member, please continue; **otherwise, read the full story here.**

If you’ve used agentic IDEs like Cursor, Trae, or Windsurf to generate Dart models, you’ve likely encountered these familiar mistakes:

// ❌ WRONG: What agents typically generate
class Todo {
  final String id;

  Todo({required this.id});

  // Inconsistent naming - takes a Map but calls it 'json'
  factory Todo.fromJson(Map<String, dynamic> json) => Todo(
    id: json['id'],
  );

  // Uses json.encode instead of jsonEncode
  String toJson() => json.encode(toMap());

  Map<String, dynamic> toMap() => {'id': id};
}

Three mistakes in one model:

  1. Wrong JSON function: Uses json.encode() instead of jsonEncode().
  2. Confused naming: The parameter is a Map<String, dynamic> but it's called jsonjson should be a String, not a Map.
  3. Mixed conventions: fromJson takes a Map, but toJson returns a String. This asymmetry is confusing and error-prone.

These mistakes propagate because agents learn from existing codebases — and most codebases have these same errors. It’s a vicious cycle of technical debt.

Here’s the correct implementation that every Dart model should follow:

import 'dart:convert' hide json;
import 'package:equatable/equatable.dart';

class Todo extends Equatable {
  final String id;
  final String title;
  final bool isDone;

  const Todo({
    required this.id,
    required this.title,
    required this.isDone,
  });

  // ─────────────────────────────────────────────
  // IMMUTABILITY & VALUE EQUALITY
  // ─────────────────────────────────────────────

  Todo copyWith({
    String? id,
    String? title,
    bool? isDone,
  }) {
    return Todo(
      id: id ?? this.id,
      title: title ?? this.title,
      isDone: isDone ?? this.isDone,
    );
  }

  // ─────────────────────────────────────────────
  // (DE)SERIALIZATION: String ↔ Object
  // ─────────────────────────────────────────────

  /// Deserializes a JSON **String** into a Todo instance
  factory Todo.fromJson(String json) => 
      Todo.fromMap(jsonDecode(json) as Map<String, dynamic>);

  /// Serializes this Todo into a JSON **String**
  String toJson() => jsonEncode(toMap());

  // ─────────────────────────────────────────────
  // MAPPING: Map ↔ Object  
  // ─────────────────────────────────────────────

  /// Creates Todo from a **Map** representation
  factory Todo.fromMap(Map<String, dynamic> map) => Todo(
        id: map['id'] as String,
        title: map['title'] as String,
        isDone: map['isDone'] as bool,
      );

  /// Converts this Todo into a **Map** representation
  Map<String, dynamic> toMap() => {
        'id': id,
        'title': title,
        'isDone': isDone,
      };

  // ─────────────────────────────────────────────
  // EQUATABLE: Value-based equality
  // ─────────────────────────────────────────────

  @override
  List<Object?> get props => [id, title, isDone];

  @override
  bool get stringify => true;
}

Rule #1: Use jsonEncode / jsonDecode, not json.encode / json.decode

Dart provides two ways to access the same functions:

import 'dart:convert';

json.encode(data);
json.decode(json);

jsonEncode(data);
jsonDecode(json);

Why jsonEncode is superior:

  1. No shadowing risk: If a local variable named json exists in your scope, json.encode() breaks. jsonEncode() always works.
  2. Community standard: Modern Flutter documentation and libraries use the top-level functions.

The implementation is identical — jsonEncode() literally just calls json.encode(). The difference is purely about code safety and readability.

Rule #2: Establish naming conventions

fromJson / toJson work with Strings. fromMap / toMap work with Maps.

Rule #3: Use Equatable

The Equatable package solves a fundamental Dart problem: reference equality vs. value equality.

The Problem: By default, Dart compares objects by memory address:

class Todo {
  final String id;
  Todo(this.id);
}

void main() {
  final a = Todo('1');
  final b = Todo('1');

  print(a == b); // false ❌ Same data, different memory addresses
}

This breaks state management in Bloc, Riverpod, or any reactive system. For example, emit(NewState()) triggers rebuild if previous != current. With reference equality, identical data causes unnecessary rebuilds.

The Solution:Equatable overrides == and hashCode to compare property values instead of memory addresses :

class Todo extends Equatable {
  final String id;
  const Todo(this.id);

  @override
  List<Object?> get props => [id]; // Compare these fields
}

void main() {
  final a = Todo('1');
  final b = Todo('1');

  print(a == b); // true ✅ Value-based equality

  // Also works in collections
  final set = {a};
  print(set.contains(b)); // true
}

Benefits:

  1. Efficient state management: No unnecessary widget rebuilds when data hasn’t changed.
  2. Collection operations: Sets and Maps work correctly with value semantics.
  3. Test assertions: expect(actual, expected) works without custom matchers.
  4. No boilerplate: Manual == and hashCode overrides are ~20 lines; Equatable is 3 lines.

Setting stringify => true provides clean debug output:

@override
bool get stringify => true;

// Without stringify: Instance of 'Todo'
// With stringify: Todo(id: 1, title: "Buy milk", isDone: false)

That’s it.

Bonus: dart-models skill.

Thank you for reading!


메타데이터
post_id
da7fdd56ae7d
slug
flutter-how-to-de-serialize-json-properly-da7fdd56ae7d
url
https://medium.com/easy-flutter/flutter-how-to-de-serialize-json-properly-da7fdd56ae7d
canonical_url
https://medium.com/easy-flutter/flutter-how-to-de-serialize-json-properly-da7fdd56ae7d
author_url
https://medium.com/@yurinovicow
status
ok
fetched_at
2026-07-31 12:13:25