JSON Serialization
Ack schemas facilitate easy conversion between your Dart models and JSON data, often used when interacting with APIs.
Validating JSON Data
The most common use case is validating incoming JSON data (e.g., from an API response). This typically involves two steps:
- Decode JSON: Use
dart:convert
to parse the JSON string into a Dart object (usuallyMap<String, dynamic>
orList
). - Validate: Pass the decoded Dart object to your Ack schema's
safeParse()
method.
import 'dart:convert';
import 'package:ack/ack.dart';
// Define userSchema using correct Ack API
final userSchema = Ack.object({
'name': Ack.string(),
'age': Ack.integer().min(0),
'email': Ack.string().email().nullable(),
});
void processApiResponse(String jsonString) {
// 1. Decode JSON string into a Dart object.
// Note: jsonDecode returns dynamic, so the structure is unknown initially.
dynamic jsonData;
try {
jsonData = jsonDecode(jsonString);
} catch (e) {
print('Failed to decode JSON: $e');
return;
}
// 2. Validate the decoded data against your defined schema.
final result = userSchema.safeParse(jsonData);
if (result.isOk) {
// Data structure and types are valid according to the schema.
final validDataMap = result.getOrThrow();
print('Valid JSON received: $validDataMap');
// Optionally convert to a typed model (see next section)
// final user = User.fromValidatedMap(validDataMap);
} else {
// Handle validation errors (see Error Handling guide)
print('Invalid JSON data: ${result.getError()}');
// Log the full error for debugging if needed:
// print('Error details: ${result.getError()}');
}
}
// Example Usage
processApiResponse('{"name": "Alice", "age": 30, "email": "alice@example.com"}');
processApiResponse('{"name": "Bob", "age": -5}'); // Invalid: age fails min(0)
processApiResponse('{"age": 25}'); // Invalid: missing required field 'name'
processApiResponse('not valid json'); // Decoding error
Learn more about Error Handling.
Working with Validated Data
After successful validation, result.getOrThrow()
returns a Map<String, dynamic>
whose structure and types match your schema. You can work with this validated data directly or convert it to a model class:
final result = userSchema.safeParse(jsonData);
if (result.isOk) {
final validData = result.getOrThrow();
// Option 1: Work directly with the validated Map
final name = validData['name'] as String;
final age = validData['age'] as int;
// Option 2: Convert to your model class using your preferred method
// - Constructor: User(name: validData['name'], age: validData['age'])
// - json_serializable: User.fromJson(validData)
// - freezed: User.fromJson(validData)
// - dart_mappable: UserMapper.fromMap(validData)
// - Manual factory: User.fromMap(validData)
}
Using Code Generation
The ack_generator
package automatically creates schemas from your annotated classes:
// user.dart
import 'package:ack_annotations/ack_annotations.dart';
part 'user.g.dart';
@AckModel()
class User {
final String name;
final String email;
final int age;
User({required this.name, required this.email, required this.age});
}
After running dart run build_runner build
, the generator creates in user.g.dart
:
- Schema constant:
final userSchema = Ack.object({...});
- Extension type:
extension type UserType(Map<String, Object?> _data) {...}
// user.g.dart (generated)
final userSchema = Ack.object({
'name': Ack.string(),
'email': Ack.string(),
'age': Ack.integer(),
});
extension type UserType(Map<String, Object?> _data) {
static UserType parse(Object? data) { ... }
static SchemaResult<UserType> safeParse(Object? data) { ... }
String get name => _data['name'] as String;
String get email => _data['email'] as String;
int get age => _data['age'] as int;
// ... plus toJson(), copyWith(), equality, etc.
}
You can use either the schema or the extension type:
import 'user.dart';
import 'dart:convert';
final jsonString = '{"name": "Alice", "email": "alice@example.com", "age": 30}';
final jsonData = jsonDecode(jsonString);
// Option 1: Use schema directly (returns Map)
final result = userSchema.safeParse(jsonData);
if (result.isOk) {
final validData = result.getOrThrow();
final user = User(
name: validData['name'] as String,
email: validData['email'] as String,
age: validData['age'] as int,
);
}
// Option 2: Use extension type (type-safe)
final result = UserType.safeParse(jsonData);
if (result.isOk) {
final user = result.getOrThrow();
print(user.name); // Type-safe String access
print(user.email); // Type-safe String access
print(user.age); // Type-safe int access
}
See the ack_generator README for more details on code generation.
Generating Extension Types from Standalone Schemas with @AckType()
When you write schemas manually (without a backing class), annotate the variable or getter with @AckType()
to generate an extension type wrapper. Unlike @AckModel
, this does not create the schema—it only adds type-safe access to your hand-written schema.
// user_schema.dart
import 'package:ack/ack.dart';
import 'package:ack_annotations/ack_annotations.dart';
part 'user_schema.g.dart';
@AckType()
final userSchema = Ack.object({
'name': Ack.string(),
'email': Ack.string().email().nullable(),
});
Running dart run build_runner build
generates in user_schema.g.dart
:
- The original schema constant remains in the generated file (so imports keep working)
- Extension type
UserType
withparse
/safeParse
, typed getters, andtoJson()
helpers
The type name is derived from the variable name by removing "Schema" suffix and adding "Type" (e.g., userSchema
→ UserType
).
import 'dart:convert';
final jsonString = '{"name": "Alice", "email": "alice@example.com"}';
final jsonData = jsonDecode(jsonString);
final result = UserType.safeParse(jsonData);
if (result.isOk) {
final user = result.getOrThrow();
print(user.name); // Type-safe String access
print(user.email); // Type-safe String? access
}
Note: Nested schemas annotated with @AckType()
generate their own extension types, but when referenced in object fields, getters return Map<String, Object?>
rather than the typed extension type.
Key Considerations
dart:convert
: Use the standarddart:convert
library (jsonEncode
,jsonDecode
) for JSON string conversion. Ack handles validation only.- Type Safety: While
jsonDecode
producesdynamic
, successful validation gives you confidence in the structure and types of the resultingMap<String, dynamic>
. - Model Conversion: After validation, how you convert the validated Map to your model class is your choice. Ack doesn't prescribe a specific pattern - use whatever works best for your project.