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.
Side-by-side comparison
Section titled “Side-by-side comparison”// TypeScript — interface + JSON.parseinterface 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 validationimport { z } from 'zod';
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(),});
const safeUser = UserSchema.parse(JSON.parse(raw));// Dart — class with fromJson / toJsonimport '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, };}
// Usagefinal map = jsonDecode(raw) as Map<String, dynamic>;final user = User.fromJson(map);print(user.name);Runnable example
Section titled “Runnable example”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)),
],
);
}
}