home / skills / yelmuratoff / agent_sync / dto-serialization

dto-serialization skill

/.ai/src/skills/dto-serialization

This skill helps you create immutable Dart DTOs and manage custom mappings without freezed or json_serializable, ensuring reliable serialization.

npx playbooks add skill yelmuratoff/agent_sync --skill dto-serialization

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
3.0 KB
---
name: dto-serialization
description: When creating/updating DTOs and (de)serialization using Dart Data Class Generator directives (no freezed/json_serializable).
---

# DTO Serialization (Dart Data Class Generator)

## When to use

- Creating a new DTO for API/persistence.
- Adding fields that require custom mapping (DateTime, Duration, enums, nested DTOs).
- Enforcing the “no freezed/json_serializable/build_runner for models” policy.

## Steps

### 1) Define the DTO as immutable

Prefer `@immutable` + `class` + `const` constructor:

```dart
import 'package:flutter/foundation.dart';

@immutable
class OrderDto {
  const OrderDto({
    required this.id,
    required this.createdAt,
    required this.timeout,
    required this.status,
  });

  final String id;
  final DateTime createdAt; // DateTime.parse(String), toIso8601String()
  final Duration timeout; // $from: Duration(milliseconds: map['timeout_ms'] as int? ?? 0), $to: timeout.inMilliseconds
  final OrderStatus status; // $from: OrderStatus.values.firstWhere((e) => e.name == (map['status'] as String?), orElse: () => OrderStatus.unknown), $to: status.name
}

enum OrderStatus { unknown, pending, paid, cancelled }
```

### 2) Use directives for non-primitive fields

Use field comments to teach the generator how to map complex types:

- `$from:` how to build the value from `map[...]`
- `$to:` how to write the value to a map
- `{value}` / `{field}` / `{key}` placeholders where supported

Avoid `Enum.values.byName(...)` for untrusted input; prefer a safe lookup with a fallback.

Example directives:

```dart
final Duration ttl; // $from: Duration(seconds: map['ttl_sec'] as int? ?? 0), $to: ttl.inSeconds
final int color; // $from: (map['color'] as int?) ?? 0xFF000000, $to: {field}
final OrderStatus status; // $from: OrderStatus.values.firstWhere((e) => e.name == (map['status'] as String?), orElse: () => OrderStatus.unknown), $to: status.name
```

### 3) Handle nested DTOs explicitly

If the payload contains nested maps/lists, keep parsing deterministic:

```dart
final List<OrderItemDto> items; // $from: ((map['items'] as List?) ?? const []).map((e) => OrderItemDto.fromMap(e as Map<String, Object?>)).toList(), $to: items.map((e) => e.toMap()).toList()
```

### 4) Generate the data class using the VS Code extension

Workflow:

- Define fields (and directives) inside the class.
- Place cursor in the class.
- Run “Generate data class” (Dart Data Class Generator).

### 5) Test serialization for critical DTOs

Write round-trip tests for DTOs used in persistence or cross-feature contracts:

```dart
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('OrderDto fromMap/toMap round-trip', () {
    final map = <String, Object?>{
      'id': '1',
      'createdAt': '2026-01-01T00:00:00.000Z',
      'timeout_ms': 1500,
      'status': 'paid',
    };

    final dto = OrderDto.fromMap(map);
    expect(dto.id, '1');
    expect(dto.timeout.inMilliseconds, 1500);

    final back = dto.toMap();
    expect(back['id'], '1');
    expect(back['timeout_ms'], 1500);
    expect(back['status'], 'paid');
  });
}
```

Overview

This skill guides creating and maintaining DTOs and their (de)serialization using Dart Data Class Generator directives (without freezed/json_serializable). It focuses on immutable DTOs, explicit field mapping for complex types, and deterministic nested parsing to keep serialization safe and testable. The guidance is practical and geared toward Flutter/Dart projects that avoid codegen build_runner models.

How this skill works

You annotate immutable classes with field-level directives in comments ($from and $to) to teach the generator how to parse and serialize non-primitive fields. The VS Code Dart Data Class Generator reads these directives and produces fromMap/toMap implementations. Round-trip tests validate critical DTOs to ensure mapping stability across changes.

When to use it

  • Creating a new DTO for API payloads or local persistence.
  • Adding fields that require custom mapping (DateTime, Duration, enums, colors).
  • Modeling nested lists or nested DTOs that require explicit parsing.
  • Enforcing a no-freezed/json_serializable policy for models.
  • Preparing DTOs used in cross-feature contracts or network boundaries.

Best practices

  • Define DTOs as immutable: use @immutable, final fields, and a const constructor.
  • Use $from and $to directives for any non-primitive field to keep mapping explicit.
  • Prefer safe enum lookup with fallback instead of Enum.byName for untrusted input.
  • Parse nested lists/maps deterministically and avoid nullable surprises with defaults.
  • Add round-trip tests for DTOs used in persistence or inter-service contracts.

Example use cases

  • OrderDto with createdAt (ISO string), timeout (milliseconds), and status enum with safe parsing.
  • Parsing a list of OrderItemDto from a nested JSON array using explicit map-to-DTO mapping.
  • Mapping a color int with a default fallback and serializing it back unchanged.
  • Serializing Duration fields to integer seconds/milliseconds for compact wire format.
  • Validating that toMap/fromMap round-trip preserves business-critical fields in tests.

FAQ

Why use directives instead of json_serializable?

Directives keep mapping logic next to the field and avoid additional build_runner tooling. They give explicit control for complex conversions and align with a no-codegen-for-models policy.

How should enums be parsed from untrusted input?

Use a safe lookup like values.firstWhere(..., orElse: () => fallback) rather than byName or byValue to avoid exceptions and provide a default.