JSON และ Typed Models
ระบบ structural type ของ TypeScript ช่วยให้คุณนิยาม interface และใช้ผลลัพธ์จาก JSON.parse เป็น type นั้นได้ทันที — compiler เชื่อ shape ที่คุณประกาศ Dart ใช้ nominal typing และไม่มี runtime structural casting คุณต้องเขียน fromJson factory constructor ที่อ่านแต่ละ field จาก decoded map โดยตรง โค้ดมากกว่า แต่ explicit มากกว่าด้วย: ถ้า field ขาดหายหรือ type ไม่ตรง คุณจะรู้ทันที ไม่ใช่ตอน call site
เปรียบเทียบแบบ side-by-side
หัวข้อที่มีชื่อว่า “เปรียบเทียบแบบ side-by-side”// 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);ตัวอย่างที่รันได้จริง
หัวข้อที่มีชื่อว่า “ตัวอย่างที่รันได้จริง”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)),
],
);
}
}