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:

  1. Decode JSON: Use dart:convert to parse the JSON string into a Dart object (usually Map<String, dynamic> or List).
  2. 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:

  1. Schema constant: final userSchema = Ack.object({...});
  2. 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 with parse/safeParse, typed getters, and toJson() helpers

The type name is derived from the variable name by removing "Schema" suffix and adding "Type" (e.g., userSchemaUserType).

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 standard dart:convert library (jsonEncode, jsonDecode) for JSON string conversion. Ack handles validation only.
  • Type Safety: While jsonDecode produces dynamic, successful validation gives you confidence in the structure and types of the resulting Map<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.