Skip to Content
DocsAckEssentialsJSON Serialization

JSON Serialization

Ack validates decoded JSON at your application boundary. On the way back out, plain schemas produce JSON-native Dart values directly, while codecs encode rich runtime values such as DateTime and Uri back to their wire representation.

Validating JSON data

Validating incoming JSON data 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 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. // jsonDecode returns an unknown structure until the schema validates it. Object? 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) { // Structure and types are valid. final validDataMap = result.getOrThrow(); print('Valid JSON received: $validDataMap'); // Pass the validated map to your own model layer, // or use an AckType-generated wrapper (see next section). } else { // Handle validation errors (see the Error Handling guide). print('Invalid JSON data: ${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 it directly, pass it into a 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) }

Parsing JSON into typed wrappers

Once a schema is annotated with @AckType() (see TypeSafe Schemas for setup), parse JSON straight into typed getters — no manual casting:

import 'dart:convert'; final jsonData = jsonDecode('{"name": "Alice", "email": "alice@example.com"}'); final result = UserType.safeParse(jsonData); if (result.isOk) { final user = result.getOrThrow()!; print(user.name); // typed String print(user.email); // typed String? }

Encoding validated data

For schemas whose runtime values are already JSON-native (String, num, bool, List, Map, and null), pass the validated value to jsonEncode:

final result = userSchema.safeParse({ 'name': 'Alice', 'age': 30, 'email': 'alice@example.com', }); if (result.isOk) { final validData = result.getOrThrow()!; final jsonString = jsonEncode(validData); print(jsonString); }

When a schema decodes boundary values into richer Dart types, encode through the schema before calling jsonEncode. This applies each codec in the nested structure and restores the JSON-safe boundary shape:

final eventSchema = Ack.object({ 'name': Ack.string(), 'startsAt': Ack.datetime(), }); final event = eventSchema.parse({ 'name': 'Launch', 'startsAt': '2026-01-15T14:00:00Z', }); final boundaryData = eventSchema.encode(event); final jsonString = jsonEncode(boundaryData);

Use safeEncode() when you want a SchemaResult instead of an exception. See Codecs for runtime invariants and custom bidirectional conversions.

Key considerations

  • dart:convert: Use jsonDecode and jsonEncode for JSON text. Ack validates values and encodes codec runtime values back to their boundary representation.
  • Type safety: jsonDecode produces dynamic, but successful validation guarantees the structure and types of the resulting Map<String, Object?>.
  • Model conversion: After validation, how you convert the validated map into an app model is up to you. Ack keeps validation and wrapper generation separate from your model layer.
Last updated on