← Back

Enhanced Enums: Eliminating 15,000 Lines of Generated Code

·architecture

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, UserRole
  • AnswerStatus, QuestionDifficulty

Medium-frequency enums (5-10 usages):

  • ImageFit, LayoutAlignment, AnimationType
  • AudioPlaybackMode, 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

  1. EnumSerializer utility: Single reusable class for all 70 enums
  2. Gradual migration: Converted 5-10 enums at a time, tested between batches
  3. Wire format preservation: Zero breaking changes to JSON API

What Could Be Improved

  1. Automated migration script: Wrote Python script late in process, should have started with automation
  2. 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.dart files deleted
  • EnumSerializer utility created and tested
  • 100% serialization compatibility verified

Future Opportunities

  1. Remove EnumSerializer: Once built_value eliminated entirely, use native enum .name
  2. Enum methods: Add behavior to enums (e.g., ContentBitType.selectText.isInteractive)
  3. 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