Skip to content

JSON and Typed Models

TypeScript’s structural type system lets you define an interface and immediately treat any JSON.parse result as that type — the compiler trusts the shape you declared. Dart uses nominal typing and has no runtime structural casting. You must write an explicit fromJson factory constructor that reads each field by name from the decoded map. It is more code, but it is also more explicit: if a field is missing or the wrong type, you find out immediately rather than at the call site.

React
// TypeScript — interface + JSON.parse
interface User {
id: number;
name: string;
email: string;
}
// TypeScript trusts the shape (no runtime check)
const user = JSON.parse(raw) as User;
console.log(user.name);
// With Zod for runtime validation
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const safeUser = UserSchema.parse(JSON.parse(raw));
Flutter
// Dart — class with fromJson / toJson
import 'dart:convert';
class User {
final int id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
// Explicit field mapping — no structural magic
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
};
}
// Usage
final map = jsonDecode(raw) as Map<String, dynamic>;
final user = User.fromJson(map);
print(user.name);
import 'dart:convert';
import 'package:flutter/material.dart';

class User {
  final int id;
  final String name;
  final String email;

  const User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'email': email,
  };
}

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'JSON Models',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const UserModelScreen(),
    );
  }
}

class UserModelScreen extends StatelessWidget {
  const UserModelScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // Hardcoded JSON string — no network needed
    const rawJson = '{"id": 42, "name": "Ada Lovelace", "email": "[email protected]"}';
    final map = jsonDecode(rawJson) as Map<String, dynamic>;
    final user = User.fromJson(map);

    return Scaffold(
      appBar: AppBar(
        title: const Text('JSON Model Demo'),
        backgroundColor: Colors.teal,
        foregroundColor: Colors.white,
      ),
      body: Center(
        child: Card(
          margin: const EdgeInsets.all(24),
          elevation: 4,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    CircleAvatar(
                      backgroundColor: Colors.teal,
                      child: Text(
                        user.name[0],
                        style: const TextStyle(color: Colors.white),
                      ),
                    ),
                    const SizedBox(width: 12),
                    Text(
                      user.name,
                      style: const TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
                const Divider(height: 28),
                _Field(label: 'ID', value: '${user.id}'),
                const SizedBox(height: 8),
                _Field(label: 'Name', value: user.name),
                const SizedBox(height: 8),
                _Field(label: 'Email', value: user.email),
                const Divider(height: 28),
                Text(
                  'toJson(): ${user.toJson()}',
                  style: const TextStyle(
                    fontSize: 12,
                    color: Colors.grey,
                    fontFamily: 'monospace',
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _Field extends StatelessWidget {
  final String label;
  final String value;
  const _Field({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SizedBox(
          width: 60,
          child: Text(
            '$label:',
            style: const TextStyle(
              fontWeight: FontWeight.w600,
              color: Colors.teal,
            ),
          ),
        ),
        Expanded(child: Text(value)),
      ],
    );
  }
}
Why can't you write `final user = jsonDecode(raw) as User;` in Dart?
What is the conventional role of a `factory` constructor in Dart model classes?
Which Dart packages can generate `fromJson`/`toJson` boilerplate automatically?