Enhanced Enums: Eliminating 15,000 Lines of Generated Code
Context
We migrated 70 EnumClass types to Dart 3 enhanced enums, deleting 68 .g.dart files containing ~15,000 lines of generated wire-name serializers. This migration eliminated one generator package while maintaining 100% backwards compatibility with existing JSON wire formats.
The Problem
The application used built_value's EnumClass pattern for 70 enum types with wire-name mappings (e.g., ContentBitType.selectText ↔ JSON "select-text"). Each enum required code generation for serialization.
Generated Code Overhead
Before: 68 .g.dart files, ~220 lines each = ~15,000 total generated lines
Example (content_bit_type.g.dart, 309 lines):
// GENERATED CODE - DO NOT MODIFY BY HAND
Serializer<ContentBitType> _$contentBitTypeSerializer =
_$ContentBitTypeSerializer();
class _$ContentBitTypeSerializer
implements PrimitiveSerializer<ContentBitType> {
@override
final Iterable<Type> types = const <Type>[ContentBitType];
@override
final String wireName = 'ContentBitType';
@override
Object serialize(Serializers serializers, ContentBitType object) {
switch (object) {
case ContentBitType.selectText:
return 'select-text';
case ContentBitType.bubblePop:
return 'bubble-pop';
// ... 43 more cases (250 lines total)
}
}
@override
ContentBitType deserialize(Serializers serializers, Object serialized) {
final String wireName = serialized as String;
switch (wireName) {
case 'select-text':
return ContentBitType.selectText;
case 'bubble-pop':
return ContentBitType.bubblePop;
// ... 43 more cases
default:
return ContentBitType.unknown;
}
}
}
EnumClass Limitations
class ContentBitType extends EnumClass {
@BuiltValueEnumConst(wireName: 'select-text')
static const ContentBitType selectText = _$selectText;
@BuiltValueEnumConst(wireName: 'bubble-pop')
static const ContentBitType bubblePop = _$bubblePop;
// ... 43 more constants
// ❌ No associated data possible
// ❌ No methods possible
// ❌ Requires build_runner
}
The Solution
Dart 3.0 enhanced enums support fields, methods, and interfaces—eliminating the need for code generation.
Migration Pattern
Before (EnumClass with code generation):
// content_bit_type.dart
abstract class ContentBitType extends EnumClass {
@BuiltValueEnumConst(wireName: 'select-text')
static const ContentBitType selectText = _$selectText;
@BuiltValueEnumConst(wireName: 'bubble-pop')
static const ContentBitType bubblePop = _$bubblePop;
// ... 43 more
static Serializer<ContentBitType> get serializer =>
_$contentBitTypeSerializer;
static BuiltSet<ContentBitType> get values => _$values;
}
// content_bit_type.g.dart (309 lines generated)
After (Dart 3 enhanced enum):
// content_bit_type.dart (67 lines, no generation)
enum ContentBitType {
selectText('select-text'),
bubblePop('bubble-pop'),
fillInBlanks('fill-in-blanks'),
calculator('calculator'),
wordBuild('word-build'),
// ... 40 more with wire names
unknown('unknown');
final String wireName;
const ContentBitType(this.wireName);
// Backwards-compatible serializer
static Serializer<ContentBitType> get serializer =>
EnumSerializer<ContentBitType>(
'ContentBitType',
values,
(e) => e.wireName,
);
// Lookup helpers
static ContentBitType valueOf(String name) =>
values.firstWhere((e) => e.name == name, orElse: () => unknown);
static ContentBitType valueFromWire(String wireName) =>
values.firstWhere((e) => e.wireName == wireName, orElse: () => unknown);
}
// Extension methods still work
extension ContentBitTypeExtension on ContentBitType {
bool get isCameraPermissionRequired =>
this == ContentBitType.recordAudio || this == ContentBitType.recordVideo;
}
EnumSerializer Utility
Created reusable EnumSerializer<T> (53 lines) for backwards compatibility:
class EnumSerializer<T extends Enum> implements PrimitiveSerializer<T> {
final String name;
final List<T> values;
final String Function(T) toWire;
const EnumSerializer(this.name, this.values, this.toWire);
@override
Iterable<Type> get types => [T];
@override
String get wireName => name;
@override
Object serialize(Serializers serializers, T object, {FullType? specifiedType}) {
return toWire(object);
}
@override
T deserialize(Serializers serializers, Object? serialized, {FullType? specifiedType}) {
final String wire = serialized as String;
return values.firstWhere(
(e) => toWire(e) == wire,
orElse: () => values.last, // Convention: last value is "unknown"
);
}
}
Usage:
enum SectionType {
progressHeader('progress_header'),
subjectsList('subjects_list'),
leaderBoard('leader_board'),
// ...
unknown('unknown');
final String wireName;
const SectionType(this.wireName);
static Serializer<SectionType> get serializer =>
EnumSerializer('SectionType', values, (e) => e.wireName);
}
Impact and Results
Code Metrics
| Metric | Before | After | Change |
|--------|--------|-------|--------|
| .g.dart files | 68 | 0 | -68 files |
| Generated lines | ~15,000 | 0 | -15,000 lines |
| Hand-written lines | ~3,500 | ~4,700 | +1,200 (explicit code) |
| Net reduction | 18,500 | 4,700 | -13,800 lines (-75%) |
| Build time | Baseline | -10% | Faster |
Enum Types Migrated (70 total)
High-frequency enums (10+ usages):
ContentBitType(45 variants)SectionType(60+ variants)AccountStatus,GenderType,UserRoleAnswerStatus,QuestionDifficulty
Medium-frequency enums (5-10 usages):
ImageFit,LayoutAlignment,AnimationTypeAudioPlaybackMode,VideoQuality
Low-frequency enums (<5 usages):
CalculatorLanguage,CurrencyType,TimeZoneType
Serialization Compatibility
100% wire format compatibility maintained:
| Enum | Wire Name Example | Before (EnumClass) | After (Enhanced Enum) |
|------|-------------------|--------------------|-----------------------|
| ContentBitType.selectText | "select-text" | ✅ | ✅ |
| ContentBitType.bubblePop | "bubble-pop" | ✅ | ✅ |
| SectionType.progressHeader | "progress_header" | ✅ | ✅ |
JSON round-trip tests: All 70 enum types pass serialization tests.
ASCII Diagram: Before vs After
BEFORE: EnumClass + Code Generation
┌─────────────────────────────────────────────────────────────┐
│ content_bit_type.dart (150 lines) │
│ │
│ abstract class ContentBitType extends EnumClass { │
│ @BuiltValueEnumConst(wireName: 'select-text') │
│ static const ContentBitType selectText = _$selectText; │
│ ... 44 more annotated constants │
│ } │
└────────────────────────┬────────────────────────────────────┘
│
│ build_runner generates ▼
│
┌────────────────────────▼────────────────────────────────────┐
│ content_bit_type.g.dart (309 lines) │
│ │
│ class _$ContentBitTypeSerializer { │
│ Object serialize(...) { │
│ switch (object) { │
│ case ContentBitType.selectText: return 'select-text'; │
│ ... 44 more cases (150 lines) │
│ } │
│ } │
│ │
│ ContentBitType deserialize(...) { │
│ switch (wireName) { │
│ case 'select-text': return ContentBitType.selectText; │
│ ... 44 more cases (150 lines) │
│ } │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
AFTER: Dart 3 Enhanced Enum (No Generation)
┌─────────────────────────────────────────────────────────────┐
│ content_bit_type.dart (67 lines) │
│ │
│ enum ContentBitType { │
│ selectText('select-text'), │
│ bubblePop('bubble-pop'), │
│ ... 43 more (1 line each) │
│ unknown('unknown'); │
│ │
│ final String wireName; │
│ const ContentBitType(this.wireName); │
│ │
│ static Serializer<ContentBitType> get serializer => │
│ EnumSerializer('ContentBitType', values, (e) => e.w...│
│ } │
└─────────────────────────────────────────────────────────────┘
│
│ Uses EnumSerializer utility ▼
│
┌─────────────────────────▼───────────────────────────────────┐
│ enum_serializer.dart (53 lines, shared by all 70 enums) │
│ │
│ class EnumSerializer<T extends Enum> { │
│ Object serialize(...) => toWire(object); │
│ T deserialize(...) => values.firstWhere(...); │
│ } │
└─────────────────────────────────────────────────────────────┘
Lessons Learned
What Worked Well
- EnumSerializer utility: Single reusable class for all 70 enums
- Gradual migration: Converted 5-10 enums at a time, tested between batches
- Wire format preservation: Zero breaking changes to JSON API
What Could Be Improved
- Automated migration script: Wrote Python script late in process, should have started with automation
- Extension migration: Some extensions had to be rewritten for enum methods
Trade-offs
- Verbose but explicit: 67 hand-written lines vs 309 generated (but easier to debug)
- One-time migration cost: 2 days of work for permanent elimination of 15K generated lines
Next Steps
Completed ✅
- All 70 EnumClass types migrated to enhanced enums
- 68
.g.dartfiles deleted - EnumSerializer utility created and tested
- 100% serialization compatibility verified
Future Opportunities
- Remove EnumSerializer: Once
built_valueeliminated entirely, use native enum.name - Enum methods: Add behavior to enums (e.g.,
ContentBitType.selectText.isInteractive) - Pattern matching: Leverage Dart 3 exhaustive switch with enums
Date: February 2026
Commit: b759954
Files Changed: 138 files, +4,700 lines, -18,500 lines (generated)
Serialization Tests: 100% passing