[code_assets] Make LinkMode and LinkModePreference closed enums
diff --git a/pkgs/code_assets/doc/schema/shared/shared_definitions.schema.json b/pkgs/code_assets/doc/schema/shared/shared_definitions.schema.json
index ac6528a..9fea64a 100644
--- a/pkgs/code_assets/doc/schema/shared/shared_definitions.schema.json
+++ b/pkgs/code_assets/doc/schema/shared/shared_definitions.schema.json
@@ -239,19 +239,12 @@
       "properties": {
         "type": {
           "type": "string",
-          "anyOf": [
-            {
-              "enum": [
-                "dynamic_loading_bundle",
-                "dynamic_loading_executable",
-                "dynamic_loading_process",
-                "dynamic_loading_system",
-                "static"
-              ]
-            },
-            {
-              "type": "string"
-            }
+          "enum": [
+            "dynamic_loading_bundle",
+            "dynamic_loading_executable",
+            "dynamic_loading_process",
+            "dynamic_loading_system",
+            "static"
           ]
         }
       },
@@ -278,18 +271,11 @@
     },
     "LinkModePreference": {
       "type": "string",
-      "anyOf": [
-        {
-          "enum": [
-            "dynamic",
-            "prefer_dynamic",
-            "prefer_static",
-            "static"
-          ]
-        },
-        {
-          "type": "string"
-        }
+      "enum": [
+        "dynamic",
+        "prefer_dynamic",
+        "prefer_static",
+        "static"
       ]
     },
     "MacOSCodeConfig": {
diff --git a/pkgs/code_assets/lib/src/code_assets/link_mode.dart b/pkgs/code_assets/lib/src/code_assets/link_mode.dart
index bd5ffa8..bafa2ed 100644
--- a/pkgs/code_assets/lib/src/code_assets/link_mode.dart
+++ b/pkgs/code_assets/lib/src/code_assets/link_mode.dart
@@ -18,7 +18,7 @@
 ///   * [DynamicLoadingSystem]
 ///
 /// See the documentation on the above classes.
-abstract final class LinkMode {
+sealed class LinkMode {
   const LinkMode._();
 
   /// Constructs a [LinkMode] from the given [json].
@@ -46,7 +46,6 @@
     final DynamicLoadingSystem system => DynamicLoadingSystemLinkModeSyntax(
       uri: system.uri,
     ),
-    _ => throw UnimplementedError('The link mode "$this" is not known'),
   };
 
   /// Converts a [LinkModeSyntax] to its corresponding [LinkMode]
@@ -59,7 +58,7 @@
     _ when linkMode.isDynamicLoadingSystemLinkMode => DynamicLoadingSystem(
       linkMode.asDynamicLoadingSystemLinkMode.uri,
     ),
-    _ => throw FormatException('The link mode "${linkMode.type}" is not known'),
+    _ => throw FormatException('Unknown LinkMode type: ${linkMode.type}'),
   };
 }
 
@@ -72,7 +71,7 @@
 /// Note: Dynamic loading is not equal to dynamic linking. Dynamic linking
 /// would have to run the linker at compile-time, which is currently not
 /// supported in the Dart and Flutter SDK.
-abstract final class DynamicLoading extends LinkMode {
+sealed class DynamicLoading extends LinkMode {
   DynamicLoading._() : super._();
 }
 
diff --git a/pkgs/code_assets/lib/src/code_assets/link_mode_preference.dart b/pkgs/code_assets/lib/src/code_assets/link_mode_preference.dart
index f92ca4e..137f3cb 100644
--- a/pkgs/code_assets/lib/src/code_assets/link_mode_preference.dart
+++ b/pkgs/code_assets/lib/src/code_assets/link_mode_preference.dart
@@ -51,20 +51,27 @@
   static const values = [dynamic, static, preferDynamic, preferStatic];
 
   @override
+  bool operator ==(Object other) =>
+      other is LinkModePreference && other.name == name;
+
+  @override
+  int get hashCode => name.hashCode;
+
+  @override
   String toString() => name;
 }
 
 /// Extension methods for [LinkModePreference] to convert to and from the
 /// syntax model.
 extension LinkModePreferenceSyntaxExtension on LinkModePreference {
-  static const _toSyntax = {
+  static final _toSyntax = {
     LinkModePreference.dynamic: LinkModePreferenceSyntax.dynamic,
     LinkModePreference.preferDynamic: LinkModePreferenceSyntax.preferDynamic,
     LinkModePreference.preferStatic: LinkModePreferenceSyntax.preferStatic,
     LinkModePreference.static: LinkModePreferenceSyntax.static,
   };
 
-  static const _fromSyntax = {
+  static final _fromSyntax = {
     LinkModePreferenceSyntax.dynamic: LinkModePreference.dynamic,
     LinkModePreferenceSyntax.preferDynamic: LinkModePreference.preferDynamic,
     LinkModePreferenceSyntax.preferStatic: LinkModePreference.preferStatic,
diff --git a/pkgs/code_assets/lib/src/code_assets/syntax.g.dart b/pkgs/code_assets/lib/src/code_assets/syntax.g.dart
index 51067a4..70c818a 100644
--- a/pkgs/code_assets/lib/src/code_assets/syntax.g.dart
+++ b/pkgs/code_assets/lib/src/code_assets/syntax.g.dart
@@ -68,7 +68,10 @@
 
   ArchitectureSyntax.unknown(this.name) : assert(!_byName.keys.contains(name));
 
-  factory ArchitectureSyntax.fromJson(String name) {
+  factory ArchitectureSyntax.fromJson(
+    String name, {
+    List<Object> path = const [],
+  }) {
     final knownValue = _byName[name];
     if (knownValue != null) {
       return knownValue;
@@ -277,7 +280,10 @@
 
   LinkModePreferenceSyntax get linkModePreference {
     final jsonValue = _reader.get<String>('link_mode_preference');
-    return LinkModePreferenceSyntax.fromJson(jsonValue);
+    return LinkModePreferenceSyntax.fromJson(
+      jsonValue,
+      path: [...path, 'link_mode_preference'],
+    );
   }
 
   set _linkModePreference(LinkModePreferenceSyntax value) {
@@ -308,7 +314,7 @@
   SanitizerSyntax? get sanitizer {
     final jsonValue = _reader.get<String?>('sanitizer');
     if (jsonValue == null) return null;
-    return SanitizerSyntax.fromJson(jsonValue);
+    return SanitizerSyntax.fromJson(jsonValue, path: [...path, 'sanitizer']);
   }
 
   set _sanitizer(SanitizerSyntax? value) {
@@ -319,7 +325,10 @@
 
   ArchitectureSyntax get targetArchitecture {
     final jsonValue = _reader.get<String>('target_architecture');
-    return ArchitectureSyntax.fromJson(jsonValue);
+    return ArchitectureSyntax.fromJson(
+      jsonValue,
+      path: [...path, 'target_architecture'],
+    );
   }
 
   set _targetArchitecture(ArchitectureSyntax value) {
@@ -331,7 +340,7 @@
 
   OSSyntax get targetOs {
     final jsonValue = _reader.get<String>('target_os');
-    return OSSyntax.fromJson(jsonValue);
+    return OSSyntax.fromJson(jsonValue, path: [...path, 'target_os']);
   }
 
   set _targetOs(OSSyntax value) {
@@ -671,7 +680,17 @@
     if (result.isStaticLinkMode) {
       return result.asStaticLinkMode;
     }
-    return result;
+    _throwFormatException(
+      result.type,
+      [...path, 'type'],
+      expectedValues: {
+        'dynamic_loading_bundle',
+        'dynamic_loading_executable',
+        'dynamic_loading_process',
+        'dynamic_loading_system',
+        'static',
+      },
+    );
   }
 
   LinkModeSyntax._fromJson(super.json, {super.path = const []})
@@ -721,15 +740,15 @@
     for (final value in values) value.name: value,
   };
 
-  LinkModePreferenceSyntax.unknown(this.name)
-    : assert(!_byName.keys.contains(name));
-
-  factory LinkModePreferenceSyntax.fromJson(String name) {
+  factory LinkModePreferenceSyntax.fromJson(
+    String name, {
+    List<Object> path = const [],
+  }) {
     final knownValue = _byName[name];
     if (knownValue != null) {
       return knownValue;
     }
-    return LinkModePreferenceSyntax.unknown(name);
+    _throwFormatException(name, path, expectedValues: _byName.keys.toSet());
   }
 
   bool get isKnown => _byName[name] != null;
@@ -916,7 +935,7 @@
 
   OSSyntax.unknown(this.name) : assert(!_byName.keys.contains(name));
 
-  factory OSSyntax.fromJson(String name) {
+  factory OSSyntax.fromJson(String name, {List<Object> path = const []}) {
     final knownValue = _byName[name];
     if (knownValue != null) {
       return knownValue;
@@ -949,7 +968,10 @@
 
   SanitizerSyntax.unknown(this.name) : assert(!_byName.keys.contains(name));
 
-  factory SanitizerSyntax.fromJson(String name) {
+  factory SanitizerSyntax.fromJson(
+    String name, {
+    List<Object> path = const [],
+  }) {
     final knownValue = _byName[name];
     if (knownValue != null) {
       return knownValue;
@@ -1057,14 +1079,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -1101,7 +1123,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -1114,7 +1136,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -1182,7 +1204,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -1212,7 +1234,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -1229,7 +1251,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -1239,7 +1266,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -1248,7 +1280,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -1261,7 +1298,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -1276,7 +1313,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -1326,30 +1363,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -1414,6 +1452,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/code_assets/lib/src/code_assets/validation.dart b/pkgs/code_assets/lib/src/code_assets/validation.dart
index 0b328cf..067d22b 100644
--- a/pkgs/code_assets/lib/src/code_assets/validation.dart
+++ b/pkgs/code_assets/lib/src/code_assets/validation.dart
@@ -262,7 +262,6 @@
   DynamicLoadingSystem _ => false,
   DynamicLoadingBundled _ => true,
   StaticLinking _ => true,
-  _ => throw UnsupportedError('Unknown link mode: $linkMode.'),
 };
 
 void _validateNoDuplicateDylibNames(
diff --git a/pkgs/code_assets/test/code_assets/asset_test.dart b/pkgs/code_assets/test/code_assets/asset_test.dart
index 4cdbba1..7e2830f 100644
--- a/pkgs/code_assets/test/code_assets/asset_test.dart
+++ b/pkgs/code_assets/test/code_assets/asset_test.dart
@@ -113,7 +113,11 @@
         predicate(
           (e) =>
               e is FormatException &&
-              e.message.contains('The link mode "wrong" is not known'),
+              e.message.contains(
+                "Unexpected value 'wrong' (String) for 'type'.",
+              ) &&
+              e.message.contains("'dynamic_loading_bundle'") &&
+              e.message.contains("'static'"),
         ),
       ),
     );
diff --git a/pkgs/code_assets/test/code_assets/link_mode_test.dart b/pkgs/code_assets/test/code_assets/link_mode_test.dart
index 0977b56..6ea1d60 100644
--- a/pkgs/code_assets/test/code_assets/link_mode_test.dart
+++ b/pkgs/code_assets/test/code_assets/link_mode_test.dart
@@ -3,10 +3,60 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:code_assets/code_assets.dart';
+import 'package:code_assets/src/code_assets/syntax.g.dart';
 import 'package:test/test.dart';
 
 void main() {
   test('LinkMode toString', () async {
     StaticLinking().toString();
   });
+
+  test('Unknown LinkMode throws FormatException', () async {
+    expect(
+      () => LinkMode.fromJson({
+        'type': 'my_custom_link_mode',
+        'extra_data': 'some_value',
+      }),
+      throwsA(isA<FormatException>()),
+    );
+  });
+
+  test('Unknown LinkModePreference throws FormatException', () async {
+    expect(
+      () => LinkModePreference.fromString('custom_pref'),
+      throwsA(
+        predicate(
+          (e) =>
+              e is FormatException &&
+              e.message.contains(
+                "Unexpected value 'custom_pref' (String) for ''."
+                " Expected one of 'dynamic', 'prefer_dynamic',"
+                " 'prefer_static', 'static'.",
+              ),
+        ),
+      ),
+    );
+  });
+
+  test(
+    'Unknown LinkModePreferenceSyntax throws FormatException with JSON path',
+    () async {
+      expect(
+        () => LinkModePreferenceSyntax.fromJson(
+          'custom_pref',
+          path: ['code', 'link_mode_preference'],
+        ),
+        throwsA(
+          predicate(
+            (e) =>
+                e is FormatException &&
+                e.message.contains(
+                  "Unexpected value 'custom_pref' (String) for"
+                  " 'code.link_mode_preference'.",
+                ),
+          ),
+        ),
+      );
+    },
+  );
 }
diff --git a/pkgs/data_assets/lib/src/data_assets/syntax.g.dart b/pkgs/data_assets/lib/src/data_assets/syntax.g.dart
index 77d8faf..c80d8f7 100644
--- a/pkgs/data_assets/lib/src/data_assets/syntax.g.dart
+++ b/pkgs/data_assets/lib/src/data_assets/syntax.g.dart
@@ -181,14 +181,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -225,7 +225,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -238,7 +238,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -306,7 +306,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -336,7 +336,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -353,7 +353,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -363,7 +368,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -372,7 +382,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -385,7 +400,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -400,7 +415,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -450,30 +465,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -538,6 +554,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/hooks/lib/src/hooks/syntax.g.dart b/pkgs/hooks/lib/src/hooks/syntax.g.dart
index 5869f83..ee67c30 100644
--- a/pkgs/hooks/lib/src/hooks/syntax.g.dart
+++ b/pkgs/hooks/lib/src/hooks/syntax.g.dart
@@ -359,7 +359,7 @@
 
   FailureTypeSyntax get type {
     final jsonValue = _reader.get<String>('type');
-    return FailureTypeSyntax.fromJson(jsonValue);
+    return FailureTypeSyntax.fromJson(jsonValue, path: [...path, 'type']);
   }
 
   set _type(FailureTypeSyntax value) {
@@ -394,7 +394,10 @@
 
   FailureTypeSyntax.unknown(this.name) : assert(!_byName.keys.contains(name));
 
-  factory FailureTypeSyntax.fromJson(String name) {
+  factory FailureTypeSyntax.fromJson(
+    String name, {
+    List<Object> path = const [],
+  }) {
     final knownValue = _byName[name];
     if (knownValue != null) {
       return knownValue;
@@ -666,7 +669,7 @@
   OutputStatusSyntax? get status {
     final jsonValue = _reader.get<String?>('status');
     if (jsonValue == null) return null;
-    return OutputStatusSyntax.fromJson(jsonValue);
+    return OutputStatusSyntax.fromJson(jsonValue, path: [...path, 'status']);
   }
 
   set status(OutputStatusSyntax? value) {
@@ -959,7 +962,10 @@
 
   OutputStatusSyntax.unknown(this.name) : assert(!_byName.keys.contains(name));
 
-  factory OutputStatusSyntax.fromJson(String name) {
+  factory OutputStatusSyntax.fromJson(
+    String name, {
+    List<Object> path = const [],
+  }) {
     final knownValue = _byName[name];
     if (knownValue != null) {
       return knownValue;
@@ -1098,14 +1104,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -1142,7 +1148,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -1155,7 +1161,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -1223,7 +1229,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -1253,7 +1259,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -1270,7 +1276,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -1280,7 +1291,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -1289,7 +1305,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -1302,7 +1323,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -1317,7 +1338,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -1367,30 +1388,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -1455,6 +1477,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/hooks_runner/lib/src/hooks_runner/syntax.g.dart b/pkgs/hooks_runner/lib/src/hooks_runner/syntax.g.dart
index 9c77aa3..480cd7f 100644
--- a/pkgs/hooks_runner/lib/src/hooks_runner/syntax.g.dart
+++ b/pkgs/hooks_runner/lib/src/hooks_runner/syntax.g.dart
@@ -212,14 +212,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -256,7 +256,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -269,7 +269,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -337,7 +337,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -367,7 +367,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -384,7 +384,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -394,7 +399,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -403,7 +413,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -416,7 +431,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -431,7 +446,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -481,30 +496,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -569,6 +585,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/json_syntax_generator/lib/src/generator/enum_class_generator.dart b/pkgs/json_syntax_generator/lib/src/generator/enum_class_generator.dart
index 5ece46a..5b58230 100644
--- a/pkgs/json_syntax_generator/lib/src/generator/enum_class_generator.dart
+++ b/pkgs/json_syntax_generator/lib/src/generator/enum_class_generator.dart
@@ -23,6 +23,16 @@
       );
     }
 
+    final unknownConstructor = classInfo.isOpen
+        ? '''
+$className.unknown(this.name) : assert(!_byName.keys.contains(name));'''
+        : '';
+    final unknownReturn = classInfo.isOpen
+        ? '''
+return $className.unknown(name);'''
+        : '''
+_throwFormatException(name, path, expectedValues: _byName.keys.toSet());''';
+
     buffer.writeln('''
 class $className {
   final String name;
@@ -39,14 +49,17 @@
     for (final value in values) value.name: value,
   };
 
-  $className.unknown(this.name) : assert(!_byName.keys.contains(name));
+  $unknownConstructor
 
-  factory $className.fromJson(String name) {
+  factory $className.fromJson(
+    String name, {
+    List<Object> path = const [],
+  }) {
     final knownValue = _byName[name];
     if(knownValue != null) {
       return knownValue;
     }
-    return $className.unknown(name);
+    $unknownReturn
   }
 
   bool get isKnown => _byName[name] != null;
diff --git a/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart b/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart
index 2df8821..b76752b 100644
--- a/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart
+++ b/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart
@@ -38,14 +38,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -82,7 +82,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -95,7 +95,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -163,7 +163,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -193,7 +193,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -210,7 +210,7 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(value, [parentKey, key], expectedType: T, pattern: valuePattern),
         );
       }
     }
@@ -220,7 +220,7 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(value, [key], expectedType: String, pattern: pattern);
     }
     return value;
   }
@@ -229,7 +229,7 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(value, [key], expectedType: String, pattern: pattern);
     }
     return value;
   }
@@ -242,7 +242,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -257,7 +257,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -307,30 +307,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -395,6 +396,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart b/pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart
index ccb499a..a8be083 100644
--- a/pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart
+++ b/pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart
@@ -66,15 +66,24 @@
 
     final className = classInfo.className;
     final factorySubclassReturns = <String>[];
+    final taggedUnionValues = <String>[];
     for (final subclass in classInfo.subclasses) {
       if (subclass.taggedUnionValue != null) {
+        taggedUnionValues.add("'${subclass.taggedUnionValue}'");
         factorySubclassReturns.add('''
         if (result.is${subclass.name}) {
           return result.as${subclass.name};
         }''');
       }
     }
+    taggedUnionValues.sort();
     final factorySubclassReturnsString = factorySubclassReturns.join('\n');
+    final expectedValuesString = taggedUnionValues.join(', ');
+    final unknownReturn = classInfo.isOpenTaggedUnion
+        ? 'return result;'
+        : '_throwFormatException(result.${classInfo.taggedUnionProperty}, '
+              "[...path, '${classInfo.taggedUnionProperty}'], "
+              'expectedValues: {$expectedValuesString});';
 
     return '''
   factory $className.fromJson(
@@ -83,7 +92,7 @@
   }) {
     final result = $className._fromJson(json, path: path);
     $factorySubclassReturnsString
-    return result;
+    $unknownReturn
   }
 ''';
   }
diff --git a/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart b/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart
index 15d93ab..9ee77a8 100644
--- a/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart
+++ b/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart
@@ -98,7 +98,7 @@
         buffer.writeln('''
 $dartType get $fieldName {
   final jsonValue = _reader.get<$dartStringType>('$jsonKey'); $earlyReturn
-  return $classType.fromJson(jsonValue);
+  return $classType.fromJson(jsonValue, path: [...path, '$jsonKey']);
 }
 ''');
         if (!property.isOverride) {
diff --git a/pkgs/json_syntax_generator/lib/src/model/class_info.dart b/pkgs/json_syntax_generator/lib/src/model/class_info.dart
index 41f9e55..1eb6ed9 100644
--- a/pkgs/json_syntax_generator/lib/src/model/class_info.dart
+++ b/pkgs/json_syntax_generator/lib/src/model/class_info.dart
@@ -47,6 +47,9 @@
   /// Only set in the parent class.
   final bool visibleTaggedUnion;
 
+  /// If the tagged union allows unknown tag values.
+  final bool isOpenTaggedUnion;
+
   bool get isTaggedUnion =>
       taggedUnionProperty != null || taggedUnionValue != null;
 
@@ -60,6 +63,7 @@
     this.taggedUnionValue,
     this.extraValidation = const [],
     this.visibleTaggedUnion = false,
+    this.isOpenTaggedUnion = true,
   }) : super() {
     superclass?.subclasses.add(this);
     if (taggedUnionValue != null) {
diff --git a/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart b/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart
index 7e9c8c6..4c4d298 100644
--- a/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart
+++ b/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart
@@ -181,6 +181,9 @@
           ? schemas.generateSubClassesKey!
           : null,
       visibleTaggedUnion: publicUnionTagValues.contains(typeName),
+      isOpenTaggedUnion:
+          !schemas.generateSubClasses ||
+          schemas.property(schemas.generateSubClassesKey!).generateOpenEnum,
       extraValidation: extraValidation,
     );
     _classes[typeName] = classInfo;
@@ -796,11 +799,15 @@
 }
 
 extension on JsonSchemas {
-  bool get generateEnum => type == SchemaType.string && anyOfs.isNotEmpty;
+  bool get generateEnum =>
+      type == SchemaType.string &&
+      (anyOfs.isNotEmpty || enumOrTaggedUnionValues.isNotEmpty);
 
   /// A class with opaque members and an `unknown` option.
   bool get generateOpenEnum =>
-      generateEnum && anyOfs.single.any((e) => e.type != null);
+      generateEnum &&
+      anyOfs.isNotEmpty &&
+      anyOfs.single.any((e) => e.type != null);
 
   /// Generate getters/setters as `Map<String, ...>`.
   bool get generateMapOf =>
@@ -815,7 +822,8 @@
     if (type != SchemaType.object) return null;
     for (final p in propertyKeys) {
       final propertySchemas = property(p);
-      if (propertySchemas.anyOfs.isNotEmpty) {
+      if (propertySchemas.anyOfs.isNotEmpty ||
+          propertySchemas.enumOrTaggedUnionValues.isNotEmpty) {
         if (propertySchemas.className != null) {
           // This is an explicit enum field, don't make the surrounding class a
           // tagged union.
@@ -904,11 +912,13 @@
   }
 
   List<String> get enumOrTaggedUnionValues => [
-    for (final schema in _schemas)
+    for (final schema in _schemas) ...[
       for (final s in schema.anyOf) ...[
         if (s.constValue is String) s.constValue as String,
         ...s.enumValues?.whereType<String>() ?? [],
       ],
+      ...schema.enumValues?.whereType<String>() ?? [],
+    ],
   ]..sort();
 }
 
diff --git a/pkgs/pub_formats/lib/src/package_config_syntax.g.dart b/pkgs/pub_formats/lib/src/package_config_syntax.g.dart
index 1f0ba6a..b1dd6e3 100644
--- a/pkgs/pub_formats/lib/src/package_config_syntax.g.dart
+++ b/pkgs/pub_formats/lib/src/package_config_syntax.g.dart
@@ -239,14 +239,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -283,7 +283,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -296,7 +296,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -364,7 +364,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -394,7 +394,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -411,7 +411,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -421,7 +426,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -430,7 +440,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -443,7 +458,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -458,7 +473,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -508,30 +523,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -596,6 +612,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/pub_formats/lib/src/package_graph_syntax.g.dart b/pkgs/pub_formats/lib/src/package_graph_syntax.g.dart
index 8a96c34..0745064 100644
--- a/pkgs/pub_formats/lib/src/package_graph_syntax.g.dart
+++ b/pkgs/pub_formats/lib/src/package_graph_syntax.g.dart
@@ -184,14 +184,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -228,7 +228,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -241,7 +241,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -309,7 +309,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -339,7 +339,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -356,7 +356,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -366,7 +371,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -375,7 +385,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -388,7 +403,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -403,7 +418,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -453,30 +468,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -541,6 +557,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart b/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart
index 49b5817..5679f13 100644
--- a/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart
+++ b/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart
@@ -28,7 +28,10 @@
   DependencyTypeSyntax.unknown(this.name)
     : assert(!_byName.keys.contains(name));
 
-  factory DependencyTypeSyntax.fromJson(String name) {
+  factory DependencyTypeSyntax.fromJson(
+    String name, {
+    List<Object> path = const [],
+  }) {
     final knownValue = _byName[name];
     if (knownValue != null) {
       return knownValue;
@@ -234,7 +237,10 @@
 
   DependencyTypeSyntax get dependency {
     final jsonValue = _reader.get<String>('dependency');
-    return DependencyTypeSyntax.fromJson(jsonValue);
+    return DependencyTypeSyntax.fromJson(
+      jsonValue,
+      path: [...path, 'dependency'],
+    );
   }
 
   set _dependency(DependencyTypeSyntax value) {
@@ -265,7 +271,7 @@
 
   PackageSourceSyntax get source {
     final jsonValue = _reader.get<String>('source');
-    return PackageSourceSyntax.fromJson(jsonValue);
+    return PackageSourceSyntax.fromJson(jsonValue, path: [...path, 'source']);
   }
 
   set _source(PackageSourceSyntax value) {
@@ -339,7 +345,10 @@
 
   PackageSourceSyntax.unknown(this.name) : assert(!_byName.keys.contains(name));
 
-  factory PackageSourceSyntax.fromJson(String name) {
+  factory PackageSourceSyntax.fromJson(
+    String name, {
+    List<Object> path = const [],
+  }) {
     final knownValue = _byName[name];
     if (knownValue != null) {
       return knownValue;
@@ -546,14 +555,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -590,7 +599,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -603,7 +612,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -671,7 +680,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -701,7 +710,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -718,7 +727,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -728,7 +742,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -737,7 +756,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -750,7 +774,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -765,7 +789,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -815,30 +839,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -903,6 +928,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/pub_formats/lib/src/pubspec_syntax.g.dart b/pkgs/pub_formats/lib/src/pubspec_syntax.g.dart
index ce001eb..17caab0 100644
--- a/pkgs/pub_formats/lib/src/pubspec_syntax.g.dart
+++ b/pkgs/pub_formats/lib/src/pubspec_syntax.g.dart
@@ -737,14 +737,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -781,7 +781,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -794,7 +794,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -862,7 +862,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -892,7 +892,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -909,7 +909,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -919,7 +924,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -928,7 +938,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -941,7 +956,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -956,7 +971,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -1006,30 +1021,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -1094,6 +1110,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,
diff --git a/pkgs/record_use/lib/src/syntax.g.dart b/pkgs/record_use/lib/src/syntax.g.dart
index 591d5f1..8ec5a47 100644
--- a/pkgs/record_use/lib/src/syntax.g.dart
+++ b/pkgs/record_use/lib/src/syntax.g.dart
@@ -2332,14 +2332,14 @@
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    throwFormatException(value, T, [key]);
+    throwFormatException(value, [key], expectedType: T);
   }
 
   List<String> validate<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return [];
     return [
-      errorString(value, T, [key]),
+      errorString(value, [key], expectedType: T),
     ];
   }
 
@@ -2376,7 +2376,7 @@
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        throwFormatException(value, T, [key, index]);
+        throwFormatException(value, [key, index], expectedType: T);
       }
     }
     return list.cast();
@@ -2389,7 +2389,7 @@
     final result = <String>[];
     for (final (index, value) in list.indexed) {
       if (value is! T) {
-        result.add(errorString(value, T, [key, index]));
+        result.add(errorString(value, [key, index], expectedType: T));
       }
     }
     return result;
@@ -2457,7 +2457,7 @@
   ) {
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        throwFormatException(value, T, [parentKey, key]);
+        throwFormatException(value, [parentKey, key], expectedType: T);
       }
     }
     return map_.cast();
@@ -2487,7 +2487,7 @@
     final result = <String>[];
     for (final MapEntry(:key, :value) in map_.entries) {
       if (value is! T) {
-        result.add(errorString(value, T, [parentKey, key]));
+        result.add(errorString(value, [parentKey, key], expectedType: T));
       }
     }
     return result;
@@ -2504,7 +2504,12 @@
           valuePattern != null &&
           !valuePattern.hasMatch(value)) {
         result.add(
-          errorString(value, T, [parentKey, key], pattern: valuePattern),
+          errorString(
+            value,
+            [parentKey, key],
+            expectedType: T,
+            pattern: valuePattern,
+          ),
         );
       }
     }
@@ -2514,7 +2519,12 @@
   String string(String key, RegExp? pattern) {
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -2523,7 +2533,12 @@
     final value = get<String?>(key);
     if (value == null) return null;
     if (pattern != null && !pattern.hasMatch(value)) {
-      throwFormatException(value, String, [key], pattern: pattern);
+      throwFormatException(
+        value,
+        [key],
+        expectedType: String,
+        pattern: pattern,
+      );
     }
     return value;
   }
@@ -2536,7 +2551,7 @@
     final value = get<String>(key);
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -2551,7 +2566,7 @@
     if (value == null) return [];
     if (pattern != null && !pattern.hasMatch(value)) {
       return [
-        errorString(value, String, [key], pattern: pattern),
+        errorString(value, [key], expectedType: String, pattern: pattern),
       ];
     }
     return [];
@@ -2601,30 +2616,31 @@
 
   Never throwFormatException(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    throw FormatException(
-      errorString(value, expectedType, pathExtension, pattern: pattern),
-    );
-  }
+    Set<String>? expectedValues,
+  }) => _throwFormatException(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String errorString(
     Object? value,
-    Type expectedType,
     List<Object> pathExtension, {
+    Type? expectedType,
     RegExp? pattern,
-  }) {
-    final pathString = _jsonPathToString(pathExtension);
-    if (value == null) {
-      return "No value was provided for '$pathString'."
-          ' Expected a $expectedType.';
-    }
-    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
-    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
-        ' Expected a $expectedType$satisfying.';
-  }
+    Set<String>? expectedValues,
+  }) => _errorString(
+    value,
+    [...path, ...pathExtension],
+    expectedType: expectedType,
+    pattern: pattern,
+    expectedValues: expectedValues,
+  );
 
   String keyErrorString(
     String key, {
@@ -2689,6 +2705,47 @@
   }
 }
 
+Never _throwFormatException(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  throw FormatException(
+    _errorString(
+      value,
+      path,
+      expectedType: expectedType,
+      pattern: pattern,
+      expectedValues: expectedValues,
+    ),
+  );
+}
+
+String _errorString(
+  Object? value,
+  List<Object> path, {
+  Type? expectedType,
+  RegExp? pattern,
+  Set<String>? expectedValues,
+}) {
+  final pathString = path.join('.');
+  final String expected;
+  if (expectedValues != null) {
+    expected = "one of ${expectedValues.map((e) => "'$e'").join(', ')}";
+  } else {
+    final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}';
+    expected = 'a $expectedType$satisfying';
+  }
+  if (value == null) {
+    return "No value was provided for '$pathString'."
+        ' Expected $expected.';
+  }
+  return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+      ' Expected $expected.';
+}
+
 void _checkArgumentMapStringElements(
   Map<String, String?>? map, {
   RegExp? valuePattern,