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á
- Como o build_runner funciona: Builder / Generator / AssetReader
- Geradores comuns: json_serializable / freezed / dart_mappable
- Mecanismo de part file: .g.dart / .freezed.dart
- Introdução ao desenvolvimento de Builders personalizados
- Cenário do Bob: DataPipeline gera automaticamente código de serialização usando json_serializable
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.
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);
}
> **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
- Código de serialização para 20 classes reduzido de 3 dias para 30 minutos
- Adicionar um novo campo requer alteração em apenas um lugar; o gerador atualiza automaticamente
- Erros de conversão de tipo podem ser detectados em tempo de compilação
3. Como o build_runner Funciona
(1) Pipeline de Geração
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
> **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
> **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
dependencies:
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.7.0
▶ Exemplo
> **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
// 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
> **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
> **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
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;
> **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
> **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
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);
}
> **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
> **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
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
> **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
> **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
# 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
> **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
> **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
// 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) => {...}
> **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
> **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
// 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,
// );
> **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
> **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
// 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
> **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
// ============================================
// 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');
}
> **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:
=== 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 watchserá 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-filterpara gerar apenas arquivos especificados.
P: Há diferença de desempenho entre
json_serializableefromJsonescrito à 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:
freezedejson_serializabledevem ser usados juntos obrigatoriamente? R: Não.json_serializablelida com serialização, enquantofreezedlida com geração de classes imutáveis. Você pode usarjson_serializablesozinho, 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 nopub.devos exige). Algumas equipes escolhem.gitignorepara arquivos gerados.
P: Como depurar código gerado? R: Abra o arquivo
.g.dartdiretamente 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_runneresource_gen? R:source_gené uma abstração de nível superior parabuild_runner, fornecendo uma API mais simples para escrever Builders personalizados. Tantojson_serializablequantofreezedsão construídos sobresource_gen.
P: Você pode personalizar os tipos para
fromJson/toJsondo@JsonKey? R: Sim. Defina uma função de nível superior ou método estático. A assinatura deve corresponder aT fromJson(Object? json)eObject toJson(T value). Depois referencie-a em@JsonKey.
📖 Resumo
build_runneré o motor de execução para geração de código Dart: lê anotações → gera códigojson_serializablegera automaticamentefromJson/toJson;@JsonKeycustomiza mapeamentofreezedgera classes de dados imutáveis:copyWith,==,hashCode,when- O mecanismo de Part file vincula código gerado com código fonte
- O DataPipeline usa
json_serializablepara eliminar serialização escrita à mão para 20 classes de modelo
📝 Exercícios
- Básico (Dificuldade ⭐): Crie um projeto Dart, adicione dependências
json_serializableebuild_runner. Adicione a anotação@JsonSerializablea uma classeProductsimples (com 3 campos), executedart run build_runner build, e examine o arquivo.g.dartgerado. - Intermediário (Dificuldade ⭐⭐): Configure
json_serializablepara um modelo aninhado (umOrdercontendoList<Product>, ondeProductcontém um enumCategory). Trate nomes de campo personalizados e valores padrão. Verifique a corretude de round-trip defromJson/toJson. - Desafio (Dificuldade ⭐⭐⭐): Combine
freezedejson_serializablepara criar um modeloOrderEventno 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.