š¤ Project: User Profile Models
š Lesson 9: JSON & Data Models
ā± 30-35 min
š Intermediate
šÆ Objective
Create nested data models for user profiles with addresses, social links, and preferences. Practice deep JSON parsing with pattern matching and null safety.
Requirements
- Create models:
UserProfile,Address,GeoLocation,SocialLinks,Preferences - Handle deeply nested JSON structures
- Use pattern matching with switch expressions
- Implement
copyWith()methods for immutable updates - Add validation in factory constructors
// Step 1: GeoLocation model
class GeoLocation {
final double latitude;
final double longitude;
GeoLocation({required this.latitude, required this.longitude});
factory GeoLocation.fromJson(Map json) {
return switch (json) {
{'lat': final double lat, 'lng': final double lng}
when lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 =>
GeoLocation(latitude: lat, longitude: lng),
_ => throw FormatException('Invalid geo location: $json'),
};
}
Map toJson() => {'lat': latitude, 'lng': longitude};
}
// Step 2: Address model
class Address {
final String street;
final String? suite;
final String city;
final String? zipcode;
final GeoLocation? geo;
Address({required this.street, this.suite, required this.city, this.zipcode, this.geo});
factory Address.fromJson(Map json) {
return switch (json) {
{'street': final String street, 'city': final String city} =>
Address(
street: street,
suite: json['suite'] as String?,
city: city,
zipcode: json['zipcode'] as String?,
geo: json['geo'] != null
? GeoLocation.fromJson(json['geo'] as Map)
: null,
),
_ => throw FormatException('Invalid address JSON'),
};
}
String get fullAddress => '${street}${suite != null ? ', $suite' : ''}, $city${zipcode != null ? ' $zipcode' : ''}';
}
// Step 3: SocialLinks model
class SocialLinks {
final String? website;
final String? twitter;
final String? github;
final String? linkedin;
SocialLinks({this.website, this.twitter, this.github, this.linkedin});
factory SocialLinks.fromJson(Map json) {
return SocialLinks(
website: json['website'] as String?,
twitter: json['twitter'] as String?,
github: json['github'] as String?,
linkedin: json['linkedin'] as String?,
);
}
bool get hasAnyLinks => website != null || twitter != null || github != null || linkedin != null;
}
// Step 4: Preferences model
class Preferences {
final bool emailNotifications;
final bool pushNotifications;
final String theme;
final String language;
Preferences({
this.emailNotifications = true,
this.pushNotifications = true,
this.theme = 'light',
this.language = 'en',
});
factory Preferences.fromJson(Map json) {
return Preferences(
emailNotifications: json['emailNotifications'] as bool? ?? true,
pushNotifications: json['pushNotifications'] as bool? ?? true,
theme: json['theme'] as String? ?? 'light',
language: json['language'] as String? ?? 'en',
);
}
Preferences copyWith({bool? emailNotifications, bool? pushNotifications, String? theme, String? language}) {
return Preferences(
emailNotifications: emailNotifications ?? this.emailNotifications,
pushNotifications: pushNotifications ?? this.pushNotifications,
theme: theme ?? this.theme,
language: language ?? this.language,
);
}
}
// Step 5: UserProfile model
class UserProfile {
final int id;
final String name;
final String username;
final String email;
final Address? address;
final String? phone;
final SocialLinks? socialLinks;
final Preferences preferences;
final DateTime? createdAt;
UserProfile({
required this.id,
required this.name,
required this.username,
required this.email,
this.address,
this.phone,
this.socialLinks,
Preferences? preferences,
this.createdAt,
}) : preferences = preferences ?? Preferences();
factory UserProfile.fromJson(Map json) {
return switch (json) {
{
'id': final int id,
'name': final String name,
'username': final String username,
'email': final String email,
} => UserProfile(
id: id,
name: name,
username: username,
email: email,
address: json['address'] != null
? Address.fromJson(json['address'] as Map)
: null,
phone: json['phone'] as String?,
socialLinks: json['socialLinks'] != null
? SocialLinks.fromJson(json['socialLinks'] as Map)
: null,
preferences: json['preferences'] != null
? Preferences.fromJson(json['preferences'] as Map)
: Preferences(),
createdAt: json['createdAt'] != null
? DateTime.tryParse(json['createdAt'] as String)
: null,
),
_ => throw FormatException('Invalid user profile JSON'),
};
}
UserProfile copyWith({String? name, String? email, Preferences? preferences}) {
return UserProfile(
id: id,
name: name ?? this.name,
username: username,
email: email ?? this.email,
address: address,
phone: phone,
socialLinks: socialLinks,
preferences: preferences ?? this.preferences,
createdAt: createdAt,
);
}
void display() {
print('š¤ ${name} (@${username})');
print(' š§ $email');
if (phone != null) print(' š $phone');
if (address != null) print(' š ${address!.fullAddress}');
if (socialLinks != null && socialLinks!.hasAnyLinks) {
print(' š Links:');
if (socialLinks!.website != null) print(' š ${socialLinks!.website}');
if (socialLinks!.twitter != null) print(' š¦ ${socialLinks!.twitter}');
if (socialLinks!.github != null) print(' š» ${socialLinks!.github}');
}
print(' āļø Theme: ${preferences.theme} | Language: ${preferences.language}');
}
}
// Step 6: Main program
void main() {
print('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
print('ā USER PROFILE MODELS ā');
print('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
final sampleJson = {
'id': 1,
'name': 'Alice Johnson',
'username': 'alice_dev',
'email': 'alice@example.com',
'phone': '+1-555-0123',
'address': {
'street': '123 Tech Lane',
'suite': 'Apt 4B',
'city': 'San Francisco',
'zipcode': '94105',
'geo': {'lat': 37.7749, 'lng': -122.4194},
},
'socialLinks': {
'website': 'https://alice.dev',
'twitter': '@alice_dev',
'github': 'alicejohnson',
},
'preferences': {
'emailNotifications': true,
'pushNotifications': false,
'theme': 'dark',
'language': 'en',
},
'createdAt': '2026-01-15T08:30:00Z',
};
try {
final user = UserProfile.fromJson(sampleJson);
user.display();
// Test copyWith
print('\nš Testing immutable update...');
final updatedUser = user.copyWith(
name: 'Alice Smith',
preferences: user.preferences.copyWith(theme: 'light'),
);
print(' Original: ${user.name} (${user.preferences.theme} theme)');
print(' Updated: ${updatedUser.name} (${updatedUser.preferences.theme} theme)');
// Test validation
print('\nš”ļø Testing validation...');
try {
final invalidGeo = {'lat': 200.0, 'lng': 0.0}; // Invalid latitude
GeoLocation.fromJson(invalidGeo);
} on FormatException catch (e) {
print(' ā
Caught invalid geo: $e');
}
print('\nā
All tests passed!');
} catch (e) {
print('ā Error: $e');
}
}