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:convertto 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, Object?> 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(Map<String, dynamic>.from(validData))
// - freezed: User.fromJson(Map<String, dynamic>.from(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({...});
// user.g.dart (generated)
final userSchema = Ack.object({
'name': Ack.string(),
'email': Ack.string(),
'age': Ack.integer(),
});
Use the generated schema for validation:
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,
);
}
See the ack_generator package 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 stays in your source file (
user_schema.dart) - Extension type
UserTypewithparse/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. When referenced in object fields or list elements, getters return the typed extension type (e.g., AddressType) when the reference can be resolved in the same library; unresolved references fall back to Map<String, Object?>.
@AckType() is only supported on top-level schema variables/getters, not classes.
Key Considerations
dart:convert: Use the standarddart:convertlibrary (jsonEncode,jsonDecode) for JSON string conversion. Ack handles validation only.- Type Safety: While
jsonDecodeproducesdynamic, successful validation gives you confidence in the structure and types of the resultingMap<String, Object?>. - 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.