JSON Serialization
Ack schemas validate JSON data and let you work with validated data directly, wrap it with generated types, or hand it off to your own model layer.
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 hand the validated map to your own model layer
// or use an AckType-generated wrapper (see next section).
} 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, pass it into your own model class, or
use a generated typed wrapper:
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: Pass the validated map into your own model layer
// - 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)
}
Generating Extension Types with @AckType()
When you write schemas manually, annotate the variable or getter with
@AckType() to generate a typed wrapper around the validated data.
// 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/safeParseand typed getters
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. Object/list references must be statically resolvable for typed
generation; unsupported or unresolved references fail generation instead of
falling back to Map<String, Object?>. For current Ack.discriminated(...)
limits, see
Type-safe Schemas.
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?>. - App-Level Model Conversion: After validation, how you turn the validated Map into an app-specific model is up to you. Ack keeps validation and wrapper generation separate from your own model layer.