š Lesson 9: JSON & Data Modelsā± 30-35 minš Intermediate
šÆ Objective
Build a complete JSON parsing system for news articles. Create model classes with factory constructors, pattern matching for safe extraction, and methods to display formatted news.
Requirements
Create model classes: NewsArticle, NewsSource, NewsResponse
Use factory constructors with fromJson() for all models
Implement pattern matching for safe JSON extraction
Create a display method that formats articles nicely
Handle missing/optional fields gracefully
Expected Output
Terminal
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā NEWS API PARSER ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š° Parsing news data...
ā Successfully parsed 3 articles
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š° Dart 3.5 Released with New Features
š 2026-05-20 | āļø Jane Doe
š dartweekly.com
š Dart 3.5 brings exciting new features including...
š·ļø #dart #programming #release
š° Flutter Conference 2026 Announced
š 2026-05-19 | āļø John Smith
š technews.io
š The annual Flutter conference will be held...
š·ļø #flutter #conference #tech
š° Google IO Highlights for Developers
š 2026-05-18 | āļø Sarah Wilson
š devblog.com
š Key announcements from Google IO that matter...
š·ļø #google #io #developer
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// Step 1: NewsSource model
class NewsSource {
final String id;
final String name;
final String? description;
final String? url;
NewsSource({
required this.id,
required this.name,
this.description,
this.url,
});
factory NewsSource.fromJson(Map json) {
return switch (json) {
{
'id': final String id,
'name': final String name,
} => NewsSource(
id: id,
name: name,
description: json['description'] as String?,
url: json['url'] as String?,
),
_ => throw FormatException('Invalid source JSON: $json'),
};
}
Map toJson() => {
'id': id,
'name': name,
if (description != null) 'description': description,
if (url != null) 'url': url,
};
}
// Step 2: NewsArticle model
class NewsArticle {
final String title;
final String? author;
final String? description;
final String? content;
final String? url;
final String? imageUrl;
final DateTime? publishedAt;
final NewsSource? source;
final List tags;
NewsArticle({
required this.title,
this.author,
this.description,
this.content,
this.url,
this.imageUrl,
this.publishedAt,
this.source,
this.tags = const [],
});
factory NewsArticle.fromJson(Map json) {
return switch (json) {
{
'title': final String title,
} => NewsArticle(
title: title,
author: json['author'] as String?,
description: json['description'] as String?,
content: json['content'] as String?,
url: json['url'] as String?,
imageUrl: json['urlToImage'] as String?,
publishedAt: json['publishedAt'] != null
? DateTime.tryParse(json['publishedAt'] as String)
: null,
source: json['source'] != null
? NewsSource.fromJson(json['source'] as Map)
: null,
tags: json['tags'] != null
? List.from(json['tags'] as List)
: [],
),
_ => throw FormatException('Invalid article JSON: $json'),
};
}
Map toJson() => {
'title': title,
if (author != null) 'author': author,
if (description != null) 'description': description,
if (content != null) 'content': content,
if (url != null) 'url': url,
if (imageUrl != null) 'urlToImage': imageUrl,
if (publishedAt != null) 'publishedAt': publishedAt!.toIso8601String(),
if (source != null) 'source': source!.toJson(),
if (tags.isNotEmpty) 'tags': tags,
};
String get formattedDate {
if (publishedAt == null) return 'Unknown date';
return '${publishedAt!.year}-${publishedAt!.month.toString().padLeft(2, '0')}-${publishedAt!.day.toString().padLeft(2, '0')}';
}
String get summary => description ?? content?.substring(0, content.length.clamp(0, 100)) ?? 'No summary';
void display() {
print('š° ${title}');
print(' š $formattedDate${author != null ? ' | āļø $author' : ''}');
if (source != null) print(' š ${source!.name}');
print(' š ${summary}');
if (tags.isNotEmpty) print(' š·ļø ${tags.map((t) => '#$t').join(' ')}');
print('');
}
}
// Step 3: NewsResponse model
class NewsResponse {
final String status;
final int totalResults;
final List articles;
NewsResponse({
required this.status,
required this.totalResults,
required this.articles,
});
factory NewsResponse.fromJson(Map json) {
return switch (json) {
{
'status': final String status,
'totalResults': final int totalResults,
'articles': final List articles,
} => NewsResponse(
status: status,
totalResults: totalResults,
articles: articles
.map((a) => NewsArticle.fromJson(a as Map))
.toList(),
),
_ => throw FormatException('Invalid response JSON'),
};
}
void displayAll() {
print('ā' * 45);
for (var article in articles) {
article.display();
}
print('ā' * 45);
}
}
// Step 4: Main program
void main() {
print('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
print('ā NEWS API PARSER ā');
print('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
// Sample JSON data
final sampleJson = {
'status': 'ok',
'totalResults': 3,
'articles': [
{
'title': 'Dart 3.5 Released with New Features',
'author': 'Jane Doe',
'description': 'Dart 3.5 brings exciting new features including...',
'url': 'https://dartweekly.com/dart-3-5',
'urlToImage': 'https://example.com/image1.jpg',
'publishedAt': '2026-05-20T10:30:00Z',
'source': {'id': 'dart-weekly', 'name': 'dartweekly.com'},
'tags': ['dart', 'programming', 'release'],
},
{
'title': 'Flutter Conference 2026 Announced',
'author': 'John Smith',
'description': 'The annual Flutter conference will be held...',
'url': 'https://technews.io/flutter-conf',
'publishedAt': '2026-05-19T14:00:00Z',
'source': {'id': 'tech-news', 'name': 'technews.io'},
'tags': ['flutter', 'conference', 'tech'],
},
{
'title': 'Google IO Highlights for Developers',
'author': 'Sarah Wilson',
'description': 'Key announcements from Google IO...',
'url': 'https://devblog.com/google-io',
'publishedAt': '2026-05-18T09:15:00Z',
'source': {'id': 'dev-blog', 'name': 'devblog.com'},
'tags': ['google', 'io', 'developer'],
},
],
};
print('š° Parsing news data...\n');
try {
final response = NewsResponse.fromJson(sampleJson);
print('ā Successfully parsed ${response.articles.length} articles\n');
response.displayAll();
// Test serialization
print('š Testing JSON serialization...');
final firstArticle = response.articles.first;
final json = firstArticle.toJson();
final reParsed = NewsArticle.fromJson(json);
print('ā Round-trip successful: ${reParsed.title}');
// Display statistics
print('\nš Statistics:');
print(' Total articles: ${response.totalResults}');
print(' Status: ${response.status}');
print(' Authors: ${response.articles.where((a) => a.author != null).length}');
print(' With images: ${response.articles.where((a) => a.imageUrl != null).length}');
} on FormatException catch (e) {
print('ā Parse error: $e');
}
}