Dart: Geração de Código em Dart — build_runner e Serialização

Última atualização: 2026-08-26

Geração de código é a arma definitiva para eliminar boilerplate — deixe máquinas escreverem código, e humanos escreverem lógica.

1. O que Você Aprenderá


2. A História Real de um Desenvolvedor

(1) O Problema: 3 Dias Gastos Serializando Manualmente 20 Classes de Modelo

O DataPipeline do Bob tem 20 classes de modelo de dados, cada uma exigindo métodos fromJson/toJson. Escrever manualmente o código de serialização para 20 classes levou 3 dias, durante os quais ocorreram 4 erros de digitação e 2 erros de conversão de tipo. Pior, toda vez que um novo campo era adicionado, 3 lugares no código tinham que ser atualizados manualmente (declaração do campo, fromJson, toJson). Uma atualização esquecida causou a falha de análise de 100.000 registros.

(2) A Solução do json_serializable

Anotar a classe de modelo com @JsonSerializable, e build_runner gera automaticamente fromJson/toJson. Adicionar um novo campo requer apenas declaração + re-execução do build, sem risco de omissão.

DART
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  Order({required this.id, required this.amount});

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

(3) Os Benefícios


3. Como o build_runner Funciona

(1) Pipeline de Geração

100%
flowchart LR
  A["model.dart<br/>@JsonSerializable"] --> B[build_runner]
  B --> C["model.g.dart<br/>fromJson/toJson"]
  B --> D["model.freezed.dart<br/>copyWith/equals"]
  A --> E[".part directive"]
  E --> C
  subgraph Generation Pipeline
    B
    C
    D
  end
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.
Componente Responsabilidade
Builder Lê arquivos fonte, determina o que gerar
Generator Contém a lógica específica de geração de código
AssetReader Lê arquivos de código fonte
AssetWriter Escreve os arquivos gerados
Part files .g.dart / .freezed.dart

4. json_serializable

(1) Configuração e Uso

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Configuração do pubspec.yaml

YAML
dependencies:
  json_annotation: ^4.8.0

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.7.0

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Classe de modelo básica

DART
// lib/src/models/order.dart
import 'package:json_annotation/json_annotation.dart';

part 'order.g.dart';

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  final String status;
  final String? category;

  Order({
    required this.id,
    required this.amount,
    required this.status,
    this.category,
  });

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}

// Executar: dart run build_runner build
// Gera order.g.dart com _$OrderFromJson e _$OrderToJson
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Mapeamento de campo personalizado

DART
import 'package:json_annotation/json_annotation.dart';

part 'product.g.dart';

@JsonSerializable()
class Product {
  @JsonKey(name: 'product_id')
  final String id;

  @JsonKey(name: 'product_name')
  final String name;

  @JsonKey(name: 'unit_price')
  final double price;

  @JsonKey(defaultValue: 'General')
  final String category;

  @JsonKey(fromJson: _dateTimeFromEpoch, toJson: _dateTimeToEpoch)
  final DateTime createdAt;

  @JsonKey(ignore: true)
  final String? cachedData;

  Product({
    required this.id,
    required this.name,
    required this.price,
    this.category = 'General',
    required this.createdAt,
    this.cachedData,
  });

  factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
  Map<String, dynamic> toJson() => _$ProductToJson(this);
}

DateTime _dateTimeFromEpoch(int epoch) => DateTime.fromMillisecondsSinceEpoch(epoch);
int _dateTimeToEpoch(DateTime dt) => dt.millisecondsSinceEpoch;
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Serialização de objeto aninhado

DART
import 'package:json_annotation/json_annotation.dart';

part 'customer.g.dart';

@JsonSerializable()
class Address {
  final String city;
  final String? state;
  final String country;

  Address({required this.city, this.state, required this.country});

  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

@JsonSerializable()
class Customer {
  final String name;
  final String email;
  final Address? address;

  Customer({required this.name, required this.email, this.address});

  factory Customer.fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);
  Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.
Parâmetro @JsonKey Significado Exemplo
name Nome da chave no JSON @JsonKey(name: 'product_id')
defaultValue Valor padrão quando ausente @JsonKey(defaultValue: 'N/A')
fromJson Função de desserialização personalizada @JsonKey(fromJson: _parse)
toJson Função de serialização personalizada @JsonKey(toJson: _format)
ignore Ignorar este campo @JsonKey(ignore: true)

5. freezed

(1) Geração de Classe de Dados Imutável

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Uso básico do freezed

DART
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';

part 'order_event.freezed.dart';
part 'order_event.g.dart';

@freezed
class OrderEvent with _$OrderEvent {
  const factory OrderEvent.created({
    required String orderId,
    required double amount,
    required DateTime timestamp,
  }) = OrderCreated;

  const factory OrderEvent.statusChanged({
    required String orderId,
    required String from,
    required String to,
  }) = OrderStatusChanged;

  const factory OrderEvent.cancelled({
    required String orderId,
    required String reason,
  }) = OrderCancelled;

  factory OrderEvent.fromJson(Map<String, dynamic> json) =>
      _$OrderEventFromJson(json);
}

// Executar: dart run build_runner build
// Gera:
// - order_event.freezed.dart: copyWith, ==, hashCode, toString, pattern matching
// - order_event.g.dart: fromJson, toJson
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.
O que freezed Gera Recurso
copyWith() Cópia imutável
== / hashCode Igualdade por valor
toString() Saída formatada
when() Callback de pattern matching
maybeWhen() Pattern matching opcional
fromJson/toJson Serialização JSON

6. Comandos do build_runner

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Comandos comuns

BASH
# Build único
dart run build_runner build

# Modo watch - recompilar ao alterar arquivos
dart run build_runner watch

# Limpar arquivos gerados
dart run build_runner clean

# Build com exclusão de arquivos obsoletos
dart run build_runner build --delete-conflicting-outputs

# Watch com exclusão
dart run build_runner watch --delete-conflicting-outputs
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.
Comando Propósito Estágio de Desenvolvimento
build Geração única Release
watch Auto-gerar ao alterar Desenvolvimento
clean Remover arquivos gerados Reset
--delete-conflicting-outputs Auto-sobrescrever conflitos Depuração

7. Mecanismo de Part File

(1) part e part of

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Relação de part file

DART
// lib/models/order.dart (arquivo fonte)
import 'package:json_annotation/json_annotation.dart';

// Declarar part files
part 'order.g.dart';        // Gerado por json_serializable
part 'order.freezed.dart';  // Gerado por freezed (se usado)

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  Order({required this.id, required this.amount});

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}

// Gerado: order.g.dart
// part of 'order.dart';
// Order _$OrderFromJson(Map<String, dynamic> json) => Order(...)
// Map<String, dynamic> _$OrderToJson(Order instance) => {...}
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.
Diretiva Localização Significado
part 'file.dart' Arquivo fonte Declara um part file
part of 'file.dart' Arquivo gerado Indica a qual arquivo fonte pertence
.g.dart Arquivo gerado Do json_serializable
.freezed.dart Arquivo gerado Do freezed

8. Introdução a Builders Personalizados

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Conceito de Builder simples

DART
// Conceito de builder personalizado (simplificado)
// Em um projeto real, isso estaria em um pacote separado

// Uma anotação personalizada
class CsvMapping {
  final String columnName;
  final bool required;

  const CsvMapping({required this.columnName, this.required = true});
}

// Modelo usando a anotação
class Order {
  @CsvMapping(columnName: 'order_id')
  final String id;

  @CsvMapping(columnName: 'total_amount', required: false)
  final double amount;

  Order({required this.id, required this.amount});
}

// O que um builder personalizado geraria:
// order.mapper.dart
// part of 'order.dart';
//
// Order OrderFromCsv(Map<String, String> row) => Order(
//   id: row['order_id'] ?? '',
//   amount: double.tryParse(row['total_amount'] ?? '0') ?? 0,
// );
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

9. Cenário do Bob: Geração de Código de Serialização do DataPipeline

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

: Definição completa de modelo

DART
// lib/src/models/order.dart
import 'package:json_annotation/json_annotation.dart';

part 'order.g.dart';

@JsonSerializable(createToJson: true, createFactory: true)
class Order {
  @JsonKey(name: 'order_id')
  final String id;

  @JsonKey(name: 'total_amount')
  final double amount;

  @JsonKey(name: 'order_status', defaultValue: 'pending')
  final String status;

  @JsonKey(name: 'product_category', required: false)
  final String? category;

  @JsonKey(name: 'discount_rate', defaultValue: 0)
  final double discountRate;

  @JsonKey(name: 'created_at')
  final DateTime createdAt;

  Order({
    required this.id,
    required this.amount,
    this.status = 'pending',
    this.category,
    this.discountRate = 0,
    DateTime? createdAt,
  }) : createdAt = createdAt ?? DateTime.now();

  // Gerado pelo build_runner
  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);

  // Propriedades computadas (não no JSON)
  double get tax => amount * 0.08;
  double get total => amount * (1 - 0.08) * (1 - discountRate);
  String get formatAmount => '\$${amount.toStringAsFixed(2)} USD';
}

// Após executar: dart run build_runner build
// O arquivo order.g.dart é gerado com:
// - _$OrderFromJson: analisa JSON para Order
// - _$OrderToJson: serializa Order para JSON
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

10. Exemplo Completo: Serialização de Modelos do DataPipeline

DART
// ============================================
// DataPipeline Model Serialization
// Modelos completos com json_serializable
// ============================================

import 'package:json_annotation/json_annotation.dart';

part 'models.g.dart';

// Modelo Order
@JsonSerializable()
class Order {
  @JsonKey(name: 'order_id')
  final String id;

  @JsonKey(name: 'total_amount')
  final double amount;

  @JsonKey(defaultValue: 'pending')
  final String status;

  @JsonKey(name: 'category')
  final String? category;

  @JsonKey(name: 'discount_rate', defaultValue: 0.0)
  final double discountRate;

  @JsonKey(name: 'region', defaultValue: 'US')
  final String region;

  Order({
    required this.id,
    required this.amount,
    this.status = 'pending',
    this.category,
    this.discountRate = 0.0,
    this.region = 'US',
  });

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);

  double get effectiveAmount => amount * (1 - discountRate);
  double get tax => effectiveAmount * 0.08;
  double get total => effectiveAmount + tax;
  String get formatTotal => '\$${total.toStringAsFixed(2)} USD';
}

// Modelo Report
@JsonSerializable()
class Report {
  final String title;
  final int totalOrders;
  final int completedOrders;
  final double totalRevenue;
  final double totalTax;
  final Map<String, double> revenueByCategory;
  final DateTime generatedAt;

  Report({
    required this.title,
    required this.totalOrders,
    required this.completedOrders,
    required this.totalRevenue,
    required this.totalTax,
    required this.revenueByCategory,
    DateTime? generatedAt,
  }) : generatedAt = generatedAt ?? DateTime.now();

  factory Report.fromJson(Map<String, dynamic> json) => _$ReportFromJson(json);
  Map<String, dynamic> toJson() => _$ReportToJson(this);

  String get formatRevenue => '\$${totalRevenue.toStringAsFixed(2)} USD';
  double get averageOrderValue => completedOrders > 0 ? totalRevenue / completedOrders : 0;
}

// Uso simulado (em produção, .g.dart seria gerado)
void main() {
  // Simulando o que o código gerado faria
  final json = {
    'order_id': 'ORD-001',
    'total_amount': 1500.0,
    'status': 'completed',
    'category': 'Electronics',
    'discount_rate': 0.1,
    'region': 'US',
  };

  // Parsing manual (simulando _$OrderFromJson)
  final order = Order(
    id: json['order_id'] as String,
    amount: (json['total_amount'] as num).toDouble(),
    status: json['status'] as String? ?? 'pending',
    category: json['category'] as String?,
    discountRate: (json['discount_rate'] as num?)?.toDouble() ?? 0.0,
    region: json['region'] as String? ?? 'US',
  );

  print('=== DataPipeline Order ===');
  print('ID:       ${order.id}');
  print('Amount:   \$${order.amount.toStringAsFixed(2)} USD');
  print('Discount: ${(order.discountRate * 100).toStringAsFixed(0)}%');
  print('Tax:      \$${order.tax.toStringAsFixed(2)} USD');
  print('Total:    ${order.formatTotal}');
  print('Category: ${order.category ?? "N/A"}');
  print('Region:   ${order.region}');

  // Simular geração de relatório
  final report = Report(
    title: 'Daily Analytics Report',
    totalOrders: 1000,
    completedOrders: 850,
    totalRevenue: 525000.0,
    totalTax: 42000.0,
    revenueByCategory: {
      'Electronics': 360000.0,
      'Clothing': 89000.0,
      'Books': 76000.0,
    },
  );

  print('\n=== Report ===');
  print('Title:    ${report.title}');
  print('Orders:   ${report.completedOrders}/${report.totalOrders}');
  print('Revenue:  ${report.formatRevenue}');
  print('Average:  \$${report.averageOrderValue.toStringAsFixed(2)} USD');
}
TEXT 📖 Somente leitura
> **Saída:** Execute em um DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x. Os resultados podem variar ligeiramente dependendo da versão do SDK.

Saída:

TEXT 📖 Somente leitura
=== DataPipeline Order ===
ID:       ORD-001
Amount:   $1500.00 USD
Discount: 10%
Tax:      $108.00 USD
Total:    $1458.00 USD
Category: Electronics
Region:   US

=== Report ===
Title:    Daily Analytics Report
Orders:   850/1000
Revenue:  $525000.00 USD
Average:  $617.65 USD

❓ Perguntas Frequentes

P: O modo build_runner watch será lento? R: O build inicial precisa escanear todos os arquivos e pode ser lento (10-30 segundos). Builds incrementais subsequentes processam apenas arquivos modificados, geralmente levando 1-3 segundos. Para projetos grandes, use --build-filter para gerar apenas arquivos especificados.

P: Há diferença de desempenho entre json_serializable e fromJson escrito à mão? R: Praticamente nenhuma. O código gerado tem qualidade similar ao código escrito à mão, e em alguns cenários pode ser até melhor (usando verificações de tipo mais precisas).

P: freezed e json_serializable devem ser usados juntos obrigatoriamente? R: Não. json_serializable lida com serialização, enquanto freezed lida com geração de classes imutáveis. Você pode usar json_serializable sozinho, ou combinar ambos.

P: Arquivos gerados devem ser commitados no controle de versão? R: Para projetos de aplicação, é recomendado commitar (para garantir que CI não precise de build_runner); para projetos de biblioteca, é recomendado commitar (pois a publicação no pub.dev os exige). Algumas equipes escolhem .gitignore para arquivos gerados.

P: Como depurar código gerado? R: Abra o arquivo .g.dart diretamente para lê-lo. O código gerado é código Dart padrão; você pode definir breakpoints, adicionar instruções print. Problemas geralmente estão na configuração de anotações, não na lógica de geração.

P: Qual a relação entre build_runner e source_gen? R: source_gen é uma abstração de nível superior para build_runner, fornecendo uma API mais simples para escrever Builders personalizados. Tanto json_serializable quanto freezed são construídos sobre source_gen.

P: Você pode personalizar os tipos para fromJson/toJson do @JsonKey? R: Sim. Defina uma função de nível superior ou método estático. A assinatura deve corresponder a T fromJson(Object? json) e Object toJson(T value). Depois referencie-a em @JsonKey.


📖 Resumo


📝 Exercícios

  1. Básico (Dificuldade ⭐): Crie um projeto Dart, adicione dependências json_serializable e build_runner. Adicione a anotação @JsonSerializable a uma classe Product simples (com 3 campos), execute dart run build_runner build, e examine o arquivo .g.dart gerado.
  2. Intermediário (Dificuldade ⭐⭐): Configure json_serializable para um modelo aninhado (um Order contendo List<Product>, onde Product contém um enum Category). Trate nomes de campo personalizados e valores padrão. Verifique a corretude de round-trip de fromJson/toJson.
  3. Desafio (Dificuldade ⭐⭐⭐): Combine freezed e json_serializable para criar um modelo OrderEvent no estilo Sealed Class para o DataPipeline (Created/StatusChanged/Cancelled). Gere classes imutáveis + serialização JSON + copyWith. Escreva testes para verificar o código gerado.

← Lição Anterior | Próxima Lição →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%