amal

Enhanced Enums: Eliminating 15,000 Lines of Generated Code

·architecture

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

MetricBeforeAfterChange
.g.dart files680-68 files
Generated lines~15,0000-15,000 lines
Hand-written lines~3,500~4,700+1,200 (explicit code)
Net reduction18,5004,700-13,800 lines (-75%)
Build timeBaseline-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:

EnumWire Name ExampleBefore (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


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