From fb95354c853c0b032aa66e8c914371dcb1653908 Mon Sep 17 00:00:00 2001
From: Daco Harkes <dacoharkes@google.com>
Date: Thu, 20 Mar 2025 11:57:12 +0100
Subject: [PATCH 1/2] [native_assets_cli] Syntax validation

---
 .../lib/src/generator/helper_library.dart     | 110 ++++++-
 .../src/generator/normal_class_generator.dart |  14 +-
 .../lib/src/generator/property_generator.dart |  80 +++++
 .../lib/src/model/property_info.dart          |  10 +
 .../lib/src/code_assets/syntax.g.dart         | 303 +++++++++++++++++-
 .../lib/src/code_assets/validation.dart       |  70 +++-
 .../lib/src/data_assets/syntax.g.dart         | 130 +++++++-
 .../lib/src/data_assets/validation.dart       |  47 ++-
 pkgs/native_assets_cli/lib/src/extension.dart |  10 +-
 .../lib/src/hook/syntax.g.dart                | 252 ++++++++++++++-
 .../native_assets_cli/lib/src/validation.dart |  29 +-
 11 files changed, 994 insertions(+), 61 deletions(-)

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 137278d643..91857c6b70 100644
--- a/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart
+++ b/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart
@@ -22,22 +22,46 @@ class JsonReader {
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    final pathString = _jsonPathToString([key]);
-    if (value == null) {
-      throw FormatException("No value was provided for '$pathString'.");
-    }
     throwFormatException(value, T, [key]);
   }
 
+  List<String> validate<T extends Object?>(String key) {
+    final value = json[key];
+    if (value is T) return [];
+    return [
+      errorString(value, T, [key]),
+    ];
+  }
+
   List<T> list<T extends Object?>(String key) =>
       _castList<T>(get<List<Object?>>(key), key);
 
+  List<String> validateList<T extends Object?>(String key) {
+    final listErrors = validate<List<Object?>>(key);
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    return _validateListElements(get<List<Object?>>(key), key);
+  }
+
   List<T>? optionalList<T extends Object?>(String key) =>
       switch (get<List<Object?>?>(key)?.cast<T>()) {
         null => null,
         final l => _castList<T>(l, key),
       };
 
+  List<String> validateOptionalList<T extends Object?>(String key) {
+    final listErrors = validate<List<Object?>?>(key);
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    final list = get<List<Object?>?>(key);
+    if (list == null) {
+      return [];
+    }
+    return _validateListElements(list, key);
+  }
+
   /// [List.cast] but with [FormatException]s.
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     var index = 0;
@@ -50,6 +74,21 @@ class JsonReader {
     return list.cast();
   }
 
+  List<String> _validateListElements<T extends Object?>(
+    List<Object?> list,
+    String key,
+  ) {
+    var index = 0;
+    final result = <String>[];
+    for (final value in list) {
+      if (value is! T) {
+        result.add(errorString(value, T, [key, index]));
+      }
+      index++;
+    }
+    return result;
+  }
+
   List<T>? optionalListParsed<T extends Object?>(
     String key,
     T Function(Object?) elementParser,
@@ -62,12 +101,32 @@ class JsonReader {
   Map<String, T> map$<T extends Object?>(String key) =>
       _castMap<T>(get<Map<String, Object?>>(key), key);
 
+  List<String> validateMap<T extends Object?>(String key) {
+    final mapErrors = validate<Map<String, Object?>>(key);
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return _validateMapElements<T>(get<Map<String, Object?>>(key), key);
+  }
+
   Map<String, T>? optionalMap<T extends Object?>(String key) =>
       switch (get<Map<String, Object?>?>(key)) {
         null => null,
         final m => _castMap<T>(m, key),
       };
 
+  List<String> validateOptionalMap<T extends Object?>(String key) {
+    final mapErrors = validate<Map<String, Object?>?>(key);
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    final map = get<Map<String, Object?>?>(key);
+    if (map == null) {
+      return [];
+    }
+    return _validateMapElements<T>(map, key);
+  }
+
   /// [Map.cast] but with [FormatException]s.
   Map<String, T> _castMap<T extends Object?>(
     Map<String, Object?> map_,
@@ -81,18 +140,40 @@ class JsonReader {
     return map_.cast();
   }
 
+  List<String> _validateMapElements<T extends Object?>(
+    Map<String, Object?> map_,
+    String parentKey,
+  ) {
+    final result = <String>[];
+    for (final MapEntry(:key, :value) in map_.entries) {
+      if (value is! T) {
+        result.add(errorString(value, T, [parentKey, key]));
+      }
+    }
+    return result;
+  }
+
   List<String>? optionalStringList(String key) => optionalList<String>(key);
 
+  List<String> validateOptionalStringList(String key) =>
+      validateOptionalList<String>(key);
+
   List<String> stringList(String key) => list<String>(key);
 
+  List<String> validateStringList(String key) => validateList<String>(key);
+
   Uri path$(String key) => _fileSystemPathToUri(get<String>(key));
 
+  List<String> validatePath(String key) => validate<String>(key);
+
   Uri? optionalPath(String key) {
     final value = get<String?>(key);
     if (value == null) return null;
     return _fileSystemPathToUri(value);
   }
 
+  List<String> validateOptionalPath(String key) => validate<String?>(key);
+
   List<Uri>? optionalPathList(String key) {
     final strings = optionalStringList(key);
     if (strings == null) {
@@ -101,6 +182,9 @@ class JsonReader {
     return [for (final string in strings) _fileSystemPathToUri(string)];
   }
 
+  List<String> validateOptionalPathList(String key) =>
+      validateOptionalStringList(key);
+
   static Uri _fileSystemPathToUri(String path) {
     if (path.endsWith(Platform.pathSeparator)) {
       return Uri.directory(path);
@@ -115,12 +199,22 @@ class JsonReader {
     Object? value,
     Type expectedType,
     List<Object> pathExtension,
+  ) {
+    throw FormatException(errorString(value, expectedType, pathExtension));
+  }
+
+  String errorString(
+    Object? value,
+    Type expectedType,
+    List<Object> pathExtension,
   ) {
     final pathString = _jsonPathToString(pathExtension);
-    throw FormatException(
-      "Unexpected value '$value' (${value.runtimeType}) for '$pathString'. "
-      'Expected a $expectedType.',
-    );
+    if (value == null) {
+      return "No value was provided for '$pathString'."
+          ' Expected a $expectedType.';
+    }
+    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+        ' Expected a $expectedType.';
   }
 }
 
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 c338997378..a4c17eb738 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
@@ -24,6 +24,7 @@ class ClassGenerator {
     final constructorSetterCalls = <String>[];
     final accessors = <String>[];
     final superParams = <String>[];
+    final validateCalls = <String>[];
 
     final propertyNames =
         {
@@ -62,6 +63,7 @@ class ClassGenerator {
       }
       if (thisClassProperty != null) {
         accessors.add(PropertyGenerator(thisClassProperty).generate());
+        validateCalls.add('...${property.validateName}()');
       }
     }
 
@@ -95,6 +97,12 @@ class $className extends $superclassName {
       buffer.writeln('''
   ${accessors.join('\n')}
 
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ${validateCalls.join(',\n')}
+  ];
+
   @override
   String toString() => '$className(\$json)';
 }
@@ -121,6 +129,10 @@ class $className {
 
   ${accessors.join('\n')}
 
+  List<String> validate() => [
+    ${validateCalls.join(',\n')}
+  ];
+
   @override
   String toString() => '$className(\$json)';
 }
@@ -132,7 +144,7 @@ class $className {
 extension ${className}Extension on $superclassName {
   bool get is$className => type == '$identifyingSubtype';
 
-  $className get as$className => $className.fromJson(json);
+  $className get as$className => $className.fromJson(json, path: path);
 }
 ''');
     }
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 5e6e30803b..093838d88f 100644
--- a/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart
+++ b/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart
@@ -85,6 +85,7 @@ class PropertyGenerator {
     final classInfo = dartType.classInfo;
     final classType = classInfo.name;
     final fieldName = property.name;
+    final validateName = property.validateName;
     final required = property.isRequired;
 
     switch (classInfo) {
@@ -106,6 +107,8 @@ set $setterName($dartType value) {
   $setter
   $sortOnKey
 }
+
+List<String> $validateName() => _reader.validate<$dartStringType>('$jsonKey');
 ''');
         }
 
@@ -127,6 +130,27 @@ set $setterName($dartType value) {
   $sortOnKey
 }
 ''');
+          if (required) {
+            buffer.writeln('''
+List<String> $validateName() {
+  final mapErrors = _reader.validate<Map<String, Object?>>('$jsonKey');
+  if (mapErrors.isNotEmpty) {
+    return mapErrors;
+  }
+  return $fieldName.validate();
+}
+''');
+          } else {
+            buffer.writeln('''
+List<String> $validateName() {
+  final mapErrors = _reader.validate<Map<String, Object?>?>('$jsonKey');
+  if (mapErrors.isNotEmpty) {
+    return mapErrors;
+  }
+  return $fieldName?.validate() ?? [];
+}
+''');
+          }
         }
     }
   }
@@ -140,6 +164,7 @@ set $setterName($dartType value) {
     String sortOnKey,
   ) {
     final fieldName = property.name;
+    final validateName = property.validateName;
 
     buffer.writeln('''
 $dartType get $fieldName => _reader.get<$dartType>('$jsonKey');
@@ -148,6 +173,8 @@ set $setterName($dartType value) {
   json.setOrRemove('$jsonKey', value);
   $sortOnKey
 }
+
+List<String> $validateName() => _reader.validate<$dartType>('$jsonKey');
 ''');
   }
 
@@ -163,6 +190,7 @@ set $setterName($dartType value) {
       throw UnimplementedError('Expected an optional property.');
     }
     final fieldName = property.name;
+    final validateName = property.validateName;
     final valueType = dartType.valueType;
 
     switch (valueType) {
@@ -175,6 +203,9 @@ set $setterName($dartType value) {
   json.setOrRemove('$jsonKey', value);
   $sortOnKey
 }
+
+List<String> $validateName() =>
+  _reader.validateOptionalMap<${dartType.valueType}>('$jsonKey');
 ''');
       case ListDartType():
         final itemType = valueType.itemType;
@@ -213,6 +244,26 @@ set $setterName($dartType value) {
   }
   $sortOnKey
 }
+
+List<String> $validateName() {
+  final mapErrors = _reader.validateOptionalMap('$jsonKey');
+  if (mapErrors.isNotEmpty) {
+    return mapErrors;
+  }
+  final jsonValue = _reader.optionalMap(
+    '$jsonKey',
+  );
+  if (jsonValue == null) {
+    return [];
+  }
+  final result = <String>[];
+  for (final list in $fieldName!.values) {
+    for (final element in list) {
+      result.addAll(element.validate());
+    }
+  }
+  return result;
+}
 ''');
       case SimpleDartType():
         switch (valueType.typeName) {
@@ -225,6 +276,8 @@ set $setterName($dartType value) {
   json.setOrRemove('$jsonKey', value);
   $sortOnKey
 }
+
+List<String> $validateName() => _reader.validateOptionalMap('$jsonKey');
 ''');
             } else {
               throw UnimplementedError(valueType.toString());
@@ -246,6 +299,7 @@ set $setterName($dartType value) {
     String sortOnKey,
   ) {
     final fieldName = property.name;
+    final validateName = property.validateName;
     final itemType = dartType.itemType;
     final typeName = itemType.toString();
     final required = property.isRequired;
@@ -279,11 +333,27 @@ set $setterName($dartType value) {
   }
   $sortOnKey
 }
+
+List<String> $validateName() {
+  final listErrors = _reader.validateOptionalList<Map<String, Object?>>(
+    '$jsonKey',
+  );
+  if (listErrors.isNotEmpty) {
+    return listErrors;
+  }
+  final elements = $fieldName;
+  if (elements == null) {
+    return [];
+  }
+  return [for (final element in elements) ...element.validate()];
+}
 ''');
       case SimpleDartType():
         switch (itemType.typeName) {
           case 'String':
             final jsonRead = required ? 'stringList' : 'optionalStringList';
+            final jsonValidate =
+                required ? 'validateStringList' : 'validateOptionalStringList';
             final setter = setOrRemove(dartType, jsonKey);
             buffer.writeln('''
 $dartType get $fieldName => _reader.$jsonRead('$jsonKey');
@@ -292,6 +362,8 @@ set $setterName($dartType value) {
   $setter
   $sortOnKey
 }
+
+List<String> $validateName() => _reader.$jsonValidate('$jsonKey');
 ''');
 
           default:
@@ -299,6 +371,8 @@ set $setterName($dartType value) {
         }
       case UriDartType():
         final jsonRead = required ? 'pathList' : 'optionalPathList';
+        final jsonValidate =
+            required ? 'validatePathList' : 'validateOptionalPathList';
         final setter = setOrRemove(dartType, jsonKey, '.toJson()');
         buffer.writeln('''
 $dartType get $fieldName => _reader.$jsonRead('$jsonKey');
@@ -307,6 +381,8 @@ set $setterName($dartType value) {
   $setter
   $sortOnKey
 }
+
+List<String> $validateName() => _reader.$jsonValidate('$jsonKey');
 ''');
       default:
         throw UnimplementedError(itemType.toString());
@@ -322,8 +398,10 @@ set $setterName($dartType value) {
     String sortOnKey,
   ) {
     final fieldName = property.name;
+    final validateName = property.validateName;
     final required = property.isRequired;
     final jsonRead = required ? r'path$' : 'optionalPath';
+    final jsonValidate = required ? r'validatePath' : 'validateOptionalPath';
     final setter = setOrRemove(dartType, jsonKey, '.toFilePath()');
     buffer.writeln('''
 $dartType get $fieldName => _reader.$jsonRead('$jsonKey');
@@ -332,6 +410,8 @@ set $setterName($dartType value) {
   $setter
   $sortOnKey
 }
+
+List<String> $validateName() => _reader.$jsonValidate('$jsonKey');
 ''');
   }
 
diff --git a/pkgs/json_syntax_generator/lib/src/model/property_info.dart b/pkgs/json_syntax_generator/lib/src/model/property_info.dart
index ac13cc174d..f0b05fb8b3 100644
--- a/pkgs/json_syntax_generator/lib/src/model/property_info.dart
+++ b/pkgs/json_syntax_generator/lib/src/model/property_info.dart
@@ -9,6 +9,9 @@ class PropertyInfo {
   /// The Dart getter and setter name.
   final String name;
 
+  /// The Dart validate method name.
+  String get validateName => '_validate${_ucFirst(name)}';
+
   /// The key in the json object for this property.
   final String jsonKey;
 
@@ -53,3 +56,10 @@ PropertyInfo(
   isRequired: $isRequired
 )''';
 }
+
+String _ucFirst(String str) {
+  if (str.isEmpty) {
+    return '';
+  }
+  return str[0].toUpperCase() + str.substring(1);
+}
diff --git a/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart b/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart
index a719c66e6b..ff06c586ff 100644
--- a/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart
+++ b/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart
@@ -28,6 +28,11 @@ class AndroidCodeConfig {
     json.setOrRemove('target_ndk_api', value);
   }
 
+  List<String> _validateTargetNdkApi() =>
+      _reader.validate<int?>('target_ndk_api');
+
+  List<String> validate() => [..._validateTargetNdkApi()];
+
   @override
   String toString() => 'AndroidCodeConfig($json)';
 }
@@ -98,6 +103,10 @@ class Asset {
     json.setOrRemove('type', value);
   }
 
+  List<String> _validateType() => _reader.validate<String?>('type');
+
+  List<String> validate() => [..._validateType()];
+
   @override
   String toString() => 'Asset($json)';
 }
@@ -147,18 +156,25 @@ class NativeCodeAsset extends Asset {
     json.setOrRemove('architecture', value?.name);
   }
 
+  List<String> _validateArchitecture() =>
+      _reader.validate<String?>('architecture');
+
   Uri? get file => _reader.optionalPath('file');
 
   set _file(Uri? value) {
     json.setOrRemove('file', value?.toFilePath());
   }
 
+  List<String> _validateFile() => _reader.validateOptionalPath('file');
+
   String get id => _reader.get<String>('id');
 
   set _id(String value) {
     json.setOrRemove('id', value);
   }
 
+  List<String> _validateId() => _reader.validate<String>('id');
+
   LinkMode get linkMode {
     final jsonValue = _reader.map$('link_mode');
     return LinkMode.fromJson(jsonValue, path: [...path, 'link_mode']);
@@ -168,6 +184,14 @@ class NativeCodeAsset extends Asset {
     json['link_mode'] = value.json;
   }
 
+  List<String> _validateLinkMode() {
+    final mapErrors = _reader.validate<Map<String, Object?>>('link_mode');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return linkMode.validate();
+  }
+
   OS get os {
     final jsonValue = _reader.get<String>('os');
     return OS.fromJson(jsonValue);
@@ -177,6 +201,18 @@ class NativeCodeAsset extends Asset {
     json['os'] = value.name;
   }
 
+  List<String> _validateOs() => _reader.validate<String>('os');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validateArchitecture(),
+    ..._validateFile(),
+    ..._validateId(),
+    ..._validateLinkMode(),
+    ..._validateOs(),
+  ];
+
   @override
   String toString() => 'NativeCodeAsset($json)';
 }
@@ -184,7 +220,8 @@ class NativeCodeAsset extends Asset {
 extension NativeCodeAssetExtension on Asset {
   bool get isNativeCodeAsset => type == 'native_code';
 
-  NativeCodeAsset get asNativeCodeAsset => NativeCodeAsset.fromJson(json);
+  NativeCodeAsset get asNativeCodeAsset =>
+      NativeCodeAsset.fromJson(json, path: path);
 }
 
 class CCompilerConfig {
@@ -220,18 +257,25 @@ class CCompilerConfig {
     json['ar'] = value.toFilePath();
   }
 
+  List<String> _validateAr() => _reader.validatePath('ar');
+
   Uri get cc => _reader.path$('cc');
 
   set _cc(Uri value) {
     json['cc'] = value.toFilePath();
   }
 
+  List<String> _validateCc() => _reader.validatePath('cc');
+
   Uri? get envScript => _reader.optionalPath('env_script');
 
   set _envScript(Uri? value) {
     json.setOrRemove('env_script', value?.toFilePath());
   }
 
+  List<String> _validateEnvScript() =>
+      _reader.validateOptionalPath('env_script');
+
   List<String>? get envScriptArguments =>
       _reader.optionalStringList('env_script_arguments');
 
@@ -239,12 +283,17 @@ class CCompilerConfig {
     json.setOrRemove('env_script_arguments', value);
   }
 
+  List<String> _validateEnvScriptArguments() =>
+      _reader.validateOptionalStringList('env_script_arguments');
+
   Uri get ld => _reader.path$('ld');
 
   set _ld(Uri value) {
     json['ld'] = value.toFilePath();
   }
 
+  List<String> _validateLd() => _reader.validatePath('ld');
+
   Windows? get windows {
     final jsonValue = _reader.optionalMap('windows');
     if (jsonValue == null) return null;
@@ -255,6 +304,23 @@ class CCompilerConfig {
     json.setOrRemove('windows', value?.json);
   }
 
+  List<String> _validateWindows() {
+    final mapErrors = _reader.validate<Map<String, Object?>?>('windows');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return windows?.validate() ?? [];
+  }
+
+  List<String> validate() => [
+    ..._validateAr(),
+    ..._validateCc(),
+    ..._validateEnvScript(),
+    ..._validateEnvScriptArguments(),
+    ..._validateLd(),
+    ..._validateWindows(),
+  ];
+
   @override
   String toString() => 'CCompilerConfig($json)';
 }
@@ -288,6 +354,18 @@ class Windows {
     json.setOrRemove('developer_command_prompt', value?.json);
   }
 
+  List<String> _validateDeveloperCommandPrompt() {
+    final mapErrors = _reader.validate<Map<String, Object?>?>(
+      'developer_command_prompt',
+    );
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return developerCommandPrompt?.validate() ?? [];
+  }
+
+  List<String> validate() => [..._validateDeveloperCommandPrompt()];
+
   @override
   String toString() => 'Windows($json)';
 }
@@ -315,12 +393,18 @@ class DeveloperCommandPrompt {
     json['arguments'] = value;
   }
 
+  List<String> _validateArguments() => _reader.validateStringList('arguments');
+
   Uri get script => _reader.path$('script');
 
   set _script(Uri value) {
     json['script'] = value.toFilePath();
   }
 
+  List<String> _validateScript() => _reader.validatePath('script');
+
+  List<String> validate() => [..._validateArguments(), ..._validateScript()];
+
   @override
   String toString() => 'DeveloperCommandPrompt($json)';
 }
@@ -364,6 +448,14 @@ class CodeConfig {
     json.setOrRemove('android', value?.json);
   }
 
+  List<String> _validateAndroid() {
+    final mapErrors = _reader.validate<Map<String, Object?>?>('android');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return android?.validate() ?? [];
+  }
+
   CCompilerConfig? get cCompiler {
     final jsonValue = _reader.optionalMap('c_compiler');
     if (jsonValue == null) return null;
@@ -374,6 +466,14 @@ class CodeConfig {
     json.setOrRemove('c_compiler', value?.json);
   }
 
+  List<String> _validateCCompiler() {
+    final mapErrors = _reader.validate<Map<String, Object?>?>('c_compiler');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return cCompiler?.validate() ?? [];
+  }
+
   IOSCodeConfig? get iOS {
     final jsonValue = _reader.optionalMap('ios');
     if (jsonValue == null) return null;
@@ -384,6 +484,14 @@ class CodeConfig {
     json.setOrRemove('ios', value?.json);
   }
 
+  List<String> _validateIOS() {
+    final mapErrors = _reader.validate<Map<String, Object?>?>('ios');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return iOS?.validate() ?? [];
+  }
+
   LinkModePreference get linkModePreference {
     final jsonValue = _reader.get<String>('link_mode_preference');
     return LinkModePreference.fromJson(jsonValue);
@@ -393,6 +501,9 @@ class CodeConfig {
     json['link_mode_preference'] = value.name;
   }
 
+  List<String> _validateLinkModePreference() =>
+      _reader.validate<String>('link_mode_preference');
+
   MacOSCodeConfig? get macOS {
     final jsonValue = _reader.optionalMap('macos');
     if (jsonValue == null) return null;
@@ -403,6 +514,14 @@ class CodeConfig {
     json.setOrRemove('macos', value?.json);
   }
 
+  List<String> _validateMacOS() {
+    final mapErrors = _reader.validate<Map<String, Object?>?>('macos');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return macOS?.validate() ?? [];
+  }
+
   Architecture get targetArchitecture {
     final jsonValue = _reader.get<String>('target_architecture');
     return Architecture.fromJson(jsonValue);
@@ -412,6 +531,9 @@ class CodeConfig {
     json['target_architecture'] = value.name;
   }
 
+  List<String> _validateTargetArchitecture() =>
+      _reader.validate<String>('target_architecture');
+
   OS get targetOs {
     final jsonValue = _reader.get<String>('target_os');
     return OS.fromJson(jsonValue);
@@ -421,6 +543,18 @@ class CodeConfig {
     json['target_os'] = value.name;
   }
 
+  List<String> _validateTargetOs() => _reader.validate<String>('target_os');
+
+  List<String> validate() => [
+    ..._validateAndroid(),
+    ..._validateCCompiler(),
+    ..._validateIOS(),
+    ..._validateLinkModePreference(),
+    ..._validateMacOS(),
+    ..._validateTargetArchitecture(),
+    ..._validateTargetOs(),
+  ];
+
   @override
   String toString() => 'CodeConfig($json)';
 }
@@ -450,6 +584,16 @@ class Config {
     json.sortOnKey();
   }
 
+  List<String> _validateCode() {
+    final mapErrors = _reader.validate<Map<String, Object?>?>('code');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return code?.validate() ?? [];
+  }
+
+  List<String> validate() => [..._validateCode()];
+
   @override
   String toString() => 'Config($json)';
 }
@@ -477,12 +621,22 @@ class IOSCodeConfig {
     json.setOrRemove('target_sdk', value);
   }
 
+  List<String> _validateTargetSdk() => _reader.validate<String?>('target_sdk');
+
   int? get targetVersion => _reader.get<int?>('target_version');
 
   set _targetVersion(int? value) {
     json.setOrRemove('target_version', value);
   }
 
+  List<String> _validateTargetVersion() =>
+      _reader.validate<int?>('target_version');
+
+  List<String> validate() => [
+    ..._validateTargetSdk(),
+    ..._validateTargetVersion(),
+  ];
+
   @override
   String toString() => 'IOSCodeConfig($json)';
 }
@@ -507,6 +661,10 @@ class LinkMode {
     json.setOrRemove('type', value);
   }
 
+  List<String> _validateType() => _reader.validate<String>('type');
+
+  List<String> validate() => [..._validateType()];
+
   @override
   String toString() => 'LinkMode($json)';
 }
@@ -517,6 +675,9 @@ class DynamicLoadingBundleLinkMode extends LinkMode {
 
   DynamicLoadingBundleLinkMode() : super(type: 'dynamic_loading_bundle');
 
+  @override
+  List<String> validate() => [...super.validate()];
+
   @override
   String toString() => 'DynamicLoadingBundleLinkMode($json)';
 }
@@ -525,7 +686,7 @@ extension DynamicLoadingBundleLinkModeExtension on LinkMode {
   bool get isDynamicLoadingBundleLinkMode => type == 'dynamic_loading_bundle';
 
   DynamicLoadingBundleLinkMode get asDynamicLoadingBundleLinkMode =>
-      DynamicLoadingBundleLinkMode.fromJson(json);
+      DynamicLoadingBundleLinkMode.fromJson(json, path: path);
 }
 
 class DynamicLoadingExecutableLinkMode extends LinkMode {
@@ -535,6 +696,9 @@ class DynamicLoadingExecutableLinkMode extends LinkMode {
   DynamicLoadingExecutableLinkMode()
     : super(type: 'dynamic_loading_executable');
 
+  @override
+  List<String> validate() => [...super.validate()];
+
   @override
   String toString() => 'DynamicLoadingExecutableLinkMode($json)';
 }
@@ -544,7 +708,7 @@ extension DynamicLoadingExecutableLinkModeExtension on LinkMode {
       type == 'dynamic_loading_executable';
 
   DynamicLoadingExecutableLinkMode get asDynamicLoadingExecutableLinkMode =>
-      DynamicLoadingExecutableLinkMode.fromJson(json);
+      DynamicLoadingExecutableLinkMode.fromJson(json, path: path);
 }
 
 class DynamicLoadingProcessLinkMode extends LinkMode {
@@ -553,6 +717,9 @@ class DynamicLoadingProcessLinkMode extends LinkMode {
 
   DynamicLoadingProcessLinkMode() : super(type: 'dynamic_loading_process');
 
+  @override
+  List<String> validate() => [...super.validate()];
+
   @override
   String toString() => 'DynamicLoadingProcessLinkMode($json)';
 }
@@ -561,7 +728,7 @@ extension DynamicLoadingProcessLinkModeExtension on LinkMode {
   bool get isDynamicLoadingProcessLinkMode => type == 'dynamic_loading_process';
 
   DynamicLoadingProcessLinkMode get asDynamicLoadingProcessLinkMode =>
-      DynamicLoadingProcessLinkMode.fromJson(json);
+      DynamicLoadingProcessLinkMode.fromJson(json, path: path);
 }
 
 class DynamicLoadingSystemLinkMode extends LinkMode {
@@ -587,6 +754,11 @@ class DynamicLoadingSystemLinkMode extends LinkMode {
     json['uri'] = value.toFilePath();
   }
 
+  List<String> _validateUri() => _reader.validatePath('uri');
+
+  @override
+  List<String> validate() => [...super.validate(), ..._validateUri()];
+
   @override
   String toString() => 'DynamicLoadingSystemLinkMode($json)';
 }
@@ -595,7 +767,7 @@ extension DynamicLoadingSystemLinkModeExtension on LinkMode {
   bool get isDynamicLoadingSystemLinkMode => type == 'dynamic_loading_system';
 
   DynamicLoadingSystemLinkMode get asDynamicLoadingSystemLinkMode =>
-      DynamicLoadingSystemLinkMode.fromJson(json);
+      DynamicLoadingSystemLinkMode.fromJson(json, path: path);
 }
 
 class StaticLinkMode extends LinkMode {
@@ -603,6 +775,9 @@ class StaticLinkMode extends LinkMode {
 
   StaticLinkMode() : super(type: 'static');
 
+  @override
+  List<String> validate() => [...super.validate()];
+
   @override
   String toString() => 'StaticLinkMode($json)';
 }
@@ -610,7 +785,8 @@ class StaticLinkMode extends LinkMode {
 extension StaticLinkModeExtension on LinkMode {
   bool get isStaticLinkMode => type == 'static';
 
-  StaticLinkMode get asStaticLinkMode => StaticLinkMode.fromJson(json);
+  StaticLinkMode get asStaticLinkMode =>
+      StaticLinkMode.fromJson(json, path: path);
 }
 
 class LinkModePreference {
@@ -673,6 +849,11 @@ class MacOSCodeConfig {
     json.setOrRemove('target_version', value);
   }
 
+  List<String> _validateTargetVersion() =>
+      _reader.validate<int?>('target_version');
+
+  List<String> validate() => [..._validateTargetVersion()];
+
   @override
   String toString() => 'MacOSCodeConfig($json)';
 }
@@ -730,22 +911,46 @@ class JsonReader {
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    final pathString = _jsonPathToString([key]);
-    if (value == null) {
-      throw FormatException("No value was provided for '$pathString'.");
-    }
     throwFormatException(value, T, [key]);
   }
 
+  List<String> validate<T extends Object?>(String key) {
+    final value = json[key];
+    if (value is T) return [];
+    return [
+      errorString(value, T, [key]),
+    ];
+  }
+
   List<T> list<T extends Object?>(String key) =>
       _castList<T>(get<List<Object?>>(key), key);
 
+  List<String> validateList<T extends Object?>(String key) {
+    final listErrors = validate<List<Object?>>(key);
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    return _validateListElements(get<List<Object?>>(key), key);
+  }
+
   List<T>? optionalList<T extends Object?>(String key) =>
       switch (get<List<Object?>?>(key)?.cast<T>()) {
         null => null,
         final l => _castList<T>(l, key),
       };
 
+  List<String> validateOptionalList<T extends Object?>(String key) {
+    final listErrors = validate<List<Object?>?>(key);
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    final list = get<List<Object?>?>(key);
+    if (list == null) {
+      return [];
+    }
+    return _validateListElements(list, key);
+  }
+
   /// [List.cast] but with [FormatException]s.
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     var index = 0;
@@ -758,6 +963,21 @@ class JsonReader {
     return list.cast();
   }
 
+  List<String> _validateListElements<T extends Object?>(
+    List<Object?> list,
+    String key,
+  ) {
+    var index = 0;
+    final result = <String>[];
+    for (final value in list) {
+      if (value is! T) {
+        result.add(errorString(value, T, [key, index]));
+      }
+      index++;
+    }
+    return result;
+  }
+
   List<T>? optionalListParsed<T extends Object?>(
     String key,
     T Function(Object?) elementParser,
@@ -770,12 +990,32 @@ class JsonReader {
   Map<String, T> map$<T extends Object?>(String key) =>
       _castMap<T>(get<Map<String, Object?>>(key), key);
 
+  List<String> validateMap<T extends Object?>(String key) {
+    final mapErrors = validate<Map<String, Object?>>(key);
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return _validateMapElements<T>(get<Map<String, Object?>>(key), key);
+  }
+
   Map<String, T>? optionalMap<T extends Object?>(String key) =>
       switch (get<Map<String, Object?>?>(key)) {
         null => null,
         final m => _castMap<T>(m, key),
       };
 
+  List<String> validateOptionalMap<T extends Object?>(String key) {
+    final mapErrors = validate<Map<String, Object?>?>(key);
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    final map = get<Map<String, Object?>?>(key);
+    if (map == null) {
+      return [];
+    }
+    return _validateMapElements<T>(map, key);
+  }
+
   /// [Map.cast] but with [FormatException]s.
   Map<String, T> _castMap<T extends Object?>(
     Map<String, Object?> map_,
@@ -789,18 +1029,40 @@ class JsonReader {
     return map_.cast();
   }
 
+  List<String> _validateMapElements<T extends Object?>(
+    Map<String, Object?> map_,
+    String parentKey,
+  ) {
+    final result = <String>[];
+    for (final MapEntry(:key, :value) in map_.entries) {
+      if (value is! T) {
+        result.add(errorString(value, T, [parentKey, key]));
+      }
+    }
+    return result;
+  }
+
   List<String>? optionalStringList(String key) => optionalList<String>(key);
 
+  List<String> validateOptionalStringList(String key) =>
+      validateOptionalList<String>(key);
+
   List<String> stringList(String key) => list<String>(key);
 
+  List<String> validateStringList(String key) => validateList<String>(key);
+
   Uri path$(String key) => _fileSystemPathToUri(get<String>(key));
 
+  List<String> validatePath(String key) => validate<String>(key);
+
   Uri? optionalPath(String key) {
     final value = get<String?>(key);
     if (value == null) return null;
     return _fileSystemPathToUri(value);
   }
 
+  List<String> validateOptionalPath(String key) => validate<String?>(key);
+
   List<Uri>? optionalPathList(String key) {
     final strings = optionalStringList(key);
     if (strings == null) {
@@ -809,6 +1071,9 @@ class JsonReader {
     return [for (final string in strings) _fileSystemPathToUri(string)];
   }
 
+  List<String> validateOptionalPathList(String key) =>
+      validateOptionalStringList(key);
+
   static Uri _fileSystemPathToUri(String path) {
     if (path.endsWith(Platform.pathSeparator)) {
       return Uri.directory(path);
@@ -823,12 +1088,22 @@ class JsonReader {
     Object? value,
     Type expectedType,
     List<Object> pathExtension,
+  ) {
+    throw FormatException(errorString(value, expectedType, pathExtension));
+  }
+
+  String errorString(
+    Object? value,
+    Type expectedType,
+    List<Object> pathExtension,
   ) {
     final pathString = _jsonPathToString(pathExtension);
-    throw FormatException(
-      "Unexpected value '$value' (${value.runtimeType}) for '$pathString'. "
-      'Expected a $expectedType.',
-    );
+    if (value == null) {
+      return "No value was provided for '$pathString'."
+          ' Expected a $expectedType.';
+    }
+    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+        ' Expected a $expectedType.';
   }
 }
 
diff --git a/pkgs/native_assets_cli/lib/src/code_assets/validation.dart b/pkgs/native_assets_cli/lib/src/code_assets/validation.dart
index 35aa793276..d68dcea6a5 100644
--- a/pkgs/native_assets_cli/lib/src/code_assets/validation.dart
+++ b/pkgs/native_assets_cli/lib/src/code_assets/validation.dart
@@ -7,16 +7,23 @@ import 'dart:io';
 import '../../code_assets_builder.dart';
 import 'config.dart';
 import 'link_mode.dart';
+import 'syntax.g.dart' as syntax;
 
 Future<ValidationErrors> validateCodeAssetBuildInput(BuildInput input) async =>
-    _validateCodeConfig('BuildInput.config.code', input.config.code);
+    _validateConfig('BuildInput.config.code', input.config);
 
 Future<ValidationErrors> validateCodeAssetLinkInput(LinkInput input) async => [
-  ..._validateCodeConfig('LinkInput.config.code', input.config.code),
+  ..._validateConfig('LinkInput.config.code', input.config),
   ...await _validateCodeAssetLinkInput(input.assets.encodedAssets),
 ];
 
-ValidationErrors _validateCodeConfig(String inputName, CodeConfig code) {
+ValidationErrors _validateConfig(String inputName, HookConfig config) {
+  final syntaxErrors = _validateConfigSyntax(config);
+  if (syntaxErrors.isNotEmpty) {
+    return syntaxErrors;
+  }
+
+  final code = config.code;
   final errors = <String>[];
   final targetOS = code.targetOS;
   switch (targetOS) {
@@ -71,13 +78,30 @@ ValidationErrors _validateCodeConfig(String inputName, CodeConfig code) {
   return errors;
 }
 
+List<String> _validateConfigSyntax(HookConfig config) {
+  final syntaxNode = syntax.Config.fromJson(config.json, path: config.path);
+  final syntaxErrors = syntaxNode.validate();
+  if (syntaxErrors.isEmpty) {
+    return [];
+  }
+  return [...syntaxErrors, _semanticValidationSkippedMessage(syntaxNode.path)];
+}
+
 Future<ValidationErrors> _validateCodeAssetLinkInput(
   List<EncodedAsset> encodedAssets,
-) async => [
-  for (final asset in encodedAssets)
-    if (asset.type == CodeAsset.type)
-      ..._validateCodeAssetFile(CodeAsset.fromEncoded(asset)),
-];
+) async {
+  final errors = <String>[];
+  for (final asset in encodedAssets) {
+    if (asset.type != CodeAsset.type) continue;
+    final syntaxErrors = _validateCodeAssetSyntax(asset);
+    if (syntaxErrors.isNotEmpty) {
+      errors.addAll(syntaxErrors);
+      continue;
+    }
+    errors.addAll(_validateCodeAssetFile(CodeAsset.fromEncoded(asset)));
+  }
+  return errors;
+}
 
 Future<ValidationErrors> validateCodeAssetBuildOutput(
   BuildInput input,
@@ -141,6 +165,11 @@ Future<ValidationErrors> _validateCodeAssetBuildOrLinkOutput(
 
   for (final asset in encodedAssets) {
     if (asset.type != CodeAsset.type) continue;
+    final syntaxErrors = _validateCodeAssetSyntax(asset);
+    if (syntaxErrors.isNotEmpty) {
+      errors.addAll(syntaxErrors);
+      continue;
+    }
     _validateCodeAsset(
       input,
       codeConfig,
@@ -158,6 +187,11 @@ Future<ValidationErrors> _validateCodeAssetBuildOrLinkOutput(
 
   for (final asset in encodedAssetsForLinking) {
     if (asset.type != CodeAsset.type) continue;
+    final syntaxErrors = _validateCodeAssetSyntax(asset);
+    if (syntaxErrors.isNotEmpty) {
+      errors.addAll(syntaxErrors);
+      continue;
+    }
     _validateCodeAsset(
       input,
       codeConfig,
@@ -172,6 +206,26 @@ Future<ValidationErrors> _validateCodeAssetBuildOrLinkOutput(
   return errors;
 }
 
+List<String> _validateCodeAssetSyntax(EncodedAsset encodedAsset) {
+  final syntaxNode = syntax.Asset.fromJson(
+    encodedAsset.toJson(),
+    path: encodedAsset.jsonPath ?? [],
+  );
+  if (!syntaxNode.isNativeCodeAsset) {
+    return [];
+  }
+  final syntaxErrors = syntaxNode.asNativeCodeAsset.validate();
+  if (syntaxErrors.isEmpty) {
+    return [];
+  }
+  return [...syntaxErrors, _semanticValidationSkippedMessage(syntaxNode.path)];
+}
+
+String _semanticValidationSkippedMessage(List<Object> jsonPath) {
+  final pathString = jsonPath.join('.');
+  return "Syntax errors in '$pathString'. Semantic validation skipped.";
+}
+
 void _validateCodeAsset(
   HookInput input,
   CodeConfig codeConfig,
diff --git a/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart b/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart
index d88a736b2d..34c1649c17 100644
--- a/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart
+++ b/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart
@@ -28,6 +28,10 @@ class Asset {
     json.setOrRemove('type', value);
   }
 
+  List<String> _validateType() => _reader.validate<String?>('type');
+
+  List<String> validate() => [..._validateType()];
+
   @override
   String toString() => 'Asset($json)';
 }
@@ -62,18 +66,32 @@ class DataAsset extends Asset {
     json['file'] = value.toFilePath();
   }
 
+  List<String> _validateFile() => _reader.validatePath('file');
+
   String get name => _reader.get<String>('name');
 
   set _name(String value) {
     json.setOrRemove('name', value);
   }
 
+  List<String> _validateName() => _reader.validate<String>('name');
+
   String get package => _reader.get<String>('package');
 
   set _package(String value) {
     json.setOrRemove('package', value);
   }
 
+  List<String> _validatePackage() => _reader.validate<String>('package');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validateFile(),
+    ..._validateName(),
+    ..._validatePackage(),
+  ];
+
   @override
   String toString() => 'DataAsset($json)';
 }
@@ -81,7 +99,7 @@ class DataAsset extends Asset {
 extension DataAssetExtension on Asset {
   bool get isDataAsset => type == 'data';
 
-  DataAsset get asDataAsset => DataAsset.fromJson(json);
+  DataAsset get asDataAsset => DataAsset.fromJson(json, path: path);
 }
 
 class JsonReader {
@@ -100,22 +118,46 @@ class JsonReader {
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    final pathString = _jsonPathToString([key]);
-    if (value == null) {
-      throw FormatException("No value was provided for '$pathString'.");
-    }
     throwFormatException(value, T, [key]);
   }
 
+  List<String> validate<T extends Object?>(String key) {
+    final value = json[key];
+    if (value is T) return [];
+    return [
+      errorString(value, T, [key]),
+    ];
+  }
+
   List<T> list<T extends Object?>(String key) =>
       _castList<T>(get<List<Object?>>(key), key);
 
+  List<String> validateList<T extends Object?>(String key) {
+    final listErrors = validate<List<Object?>>(key);
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    return _validateListElements(get<List<Object?>>(key), key);
+  }
+
   List<T>? optionalList<T extends Object?>(String key) =>
       switch (get<List<Object?>?>(key)?.cast<T>()) {
         null => null,
         final l => _castList<T>(l, key),
       };
 
+  List<String> validateOptionalList<T extends Object?>(String key) {
+    final listErrors = validate<List<Object?>?>(key);
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    final list = get<List<Object?>?>(key);
+    if (list == null) {
+      return [];
+    }
+    return _validateListElements(list, key);
+  }
+
   /// [List.cast] but with [FormatException]s.
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     var index = 0;
@@ -128,6 +170,21 @@ class JsonReader {
     return list.cast();
   }
 
+  List<String> _validateListElements<T extends Object?>(
+    List<Object?> list,
+    String key,
+  ) {
+    var index = 0;
+    final result = <String>[];
+    for (final value in list) {
+      if (value is! T) {
+        result.add(errorString(value, T, [key, index]));
+      }
+      index++;
+    }
+    return result;
+  }
+
   List<T>? optionalListParsed<T extends Object?>(
     String key,
     T Function(Object?) elementParser,
@@ -140,12 +197,32 @@ class JsonReader {
   Map<String, T> map$<T extends Object?>(String key) =>
       _castMap<T>(get<Map<String, Object?>>(key), key);
 
+  List<String> validateMap<T extends Object?>(String key) {
+    final mapErrors = validate<Map<String, Object?>>(key);
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return _validateMapElements<T>(get<Map<String, Object?>>(key), key);
+  }
+
   Map<String, T>? optionalMap<T extends Object?>(String key) =>
       switch (get<Map<String, Object?>?>(key)) {
         null => null,
         final m => _castMap<T>(m, key),
       };
 
+  List<String> validateOptionalMap<T extends Object?>(String key) {
+    final mapErrors = validate<Map<String, Object?>?>(key);
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    final map = get<Map<String, Object?>?>(key);
+    if (map == null) {
+      return [];
+    }
+    return _validateMapElements<T>(map, key);
+  }
+
   /// [Map.cast] but with [FormatException]s.
   Map<String, T> _castMap<T extends Object?>(
     Map<String, Object?> map_,
@@ -159,18 +236,40 @@ class JsonReader {
     return map_.cast();
   }
 
+  List<String> _validateMapElements<T extends Object?>(
+    Map<String, Object?> map_,
+    String parentKey,
+  ) {
+    final result = <String>[];
+    for (final MapEntry(:key, :value) in map_.entries) {
+      if (value is! T) {
+        result.add(errorString(value, T, [parentKey, key]));
+      }
+    }
+    return result;
+  }
+
   List<String>? optionalStringList(String key) => optionalList<String>(key);
 
+  List<String> validateOptionalStringList(String key) =>
+      validateOptionalList<String>(key);
+
   List<String> stringList(String key) => list<String>(key);
 
+  List<String> validateStringList(String key) => validateList<String>(key);
+
   Uri path$(String key) => _fileSystemPathToUri(get<String>(key));
 
+  List<String> validatePath(String key) => validate<String>(key);
+
   Uri? optionalPath(String key) {
     final value = get<String?>(key);
     if (value == null) return null;
     return _fileSystemPathToUri(value);
   }
 
+  List<String> validateOptionalPath(String key) => validate<String?>(key);
+
   List<Uri>? optionalPathList(String key) {
     final strings = optionalStringList(key);
     if (strings == null) {
@@ -179,6 +278,9 @@ class JsonReader {
     return [for (final string in strings) _fileSystemPathToUri(string)];
   }
 
+  List<String> validateOptionalPathList(String key) =>
+      validateOptionalStringList(key);
+
   static Uri _fileSystemPathToUri(String path) {
     if (path.endsWith(Platform.pathSeparator)) {
       return Uri.directory(path);
@@ -193,12 +295,22 @@ class JsonReader {
     Object? value,
     Type expectedType,
     List<Object> pathExtension,
+  ) {
+    throw FormatException(errorString(value, expectedType, pathExtension));
+  }
+
+  String errorString(
+    Object? value,
+    Type expectedType,
+    List<Object> pathExtension,
   ) {
     final pathString = _jsonPathToString(pathExtension);
-    throw FormatException(
-      "Unexpected value '$value' (${value.runtimeType}) for '$pathString'. "
-      'Expected a $expectedType.',
-    );
+    if (value == null) {
+      return "No value was provided for '$pathString'."
+          ' Expected a $expectedType.';
+    }
+    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+        ' Expected a $expectedType.';
   }
 }
 
diff --git a/pkgs/native_assets_cli/lib/src/data_assets/validation.dart b/pkgs/native_assets_cli/lib/src/data_assets/validation.dart
index 234310bd1e..dc6801a55e 100644
--- a/pkgs/native_assets_cli/lib/src/data_assets/validation.dart
+++ b/pkgs/native_assets_cli/lib/src/data_assets/validation.dart
@@ -5,18 +5,28 @@
 import 'dart:io';
 
 import '../../data_assets_builder.dart';
+import 'syntax.g.dart' as syntax;
 
 Future<ValidationErrors> validateDataAssetBuildInput(BuildInput input) async =>
     const [];
 
 Future<ValidationErrors> validateDataAssetLinkInput(LinkInput input) async {
-  final errors = <String>[
-    for (final asset in input.assets.data)
-      ..._validateFile(
-        'LinkInput.assets.data asset "${asset.id}" file',
-        asset.file,
+  final errors = <String>[];
+  for (final asset in input.assets.encodedAssets) {
+    final syntaxErrors = _validateDataAssetSyntax(asset);
+    if (asset.type != DataAsset.type) continue;
+    if (syntaxErrors.isNotEmpty) {
+      errors.addAll(syntaxErrors);
+      continue;
+    }
+    final dataAsset = DataAsset.fromEncoded(asset);
+    errors.addAll(
+      _validateFile(
+        'LinkInput.assets.data asset "${dataAsset.id}" file',
+        dataAsset.file,
       ),
-  ];
+    );
+  }
   return errors;
 }
 
@@ -48,6 +58,11 @@ Future<ValidationErrors> _validateDataAssetBuildOrLinkOutput(
 
   for (final asset in encodedAssets) {
     if (asset.type != DataAsset.type) continue;
+    final syntaxErrors = _validateDataAssetSyntax(asset);
+    if (syntaxErrors.isNotEmpty) {
+      errors.addAll(syntaxErrors);
+      continue;
+    }
     _validateDataAsset(
       input,
       DataAsset.fromEncoded(asset),
@@ -76,6 +91,26 @@ void _validateDataAsset(
   errors.addAll(_validateFile('Data asset ${dataAsset.name} file', file));
 }
 
+List<String> _validateDataAssetSyntax(EncodedAsset encodedAsset) {
+  final syntaxNode = syntax.Asset.fromJson(
+    encodedAsset.toJson(),
+    path: encodedAsset.jsonPath ?? [],
+  );
+  if (!syntaxNode.isDataAsset) {
+    return [];
+  }
+  final syntaxErrors = syntaxNode.asDataAsset.validate();
+  if (syntaxErrors.isEmpty) {
+    return [];
+  }
+  return [...syntaxErrors, semanticValidationSkippedMessage(syntaxNode.path)];
+}
+
+String semanticValidationSkippedMessage(List<Object> jsonPath) {
+  final pathString = jsonPath.join('.');
+  return "Syntax errors in '$pathString'. Semantic validation skipped.";
+}
+
 ValidationErrors _validateFile(
   String name,
   Uri uri, {
diff --git a/pkgs/native_assets_cli/lib/src/extension.dart b/pkgs/native_assets_cli/lib/src/extension.dart
index ed1231d325..384c0a323c 100644
--- a/pkgs/native_assets_cli/lib/src/extension.dart
+++ b/pkgs/native_assets_cli/lib/src/extension.dart
@@ -14,7 +14,7 @@ typedef ValidationErrors = List<String>;
 ///
 /// The extension contains callbacks to
 /// 1. setup the input, and
-/// 2. validate semantic constraints.
+/// 2. validate syntactic and semantic constraints.
 abstract interface class ProtocolExtension {
   /// The [HookConfig.buildAssetTypes] this extension adds.
   List<String> get buildAssetTypes;
@@ -25,19 +25,19 @@ abstract interface class ProtocolExtension {
   /// Setup the [HookConfig] for this extension.
   void setupLinkInput(LinkInputBuilder input);
 
-  /// Reports semantic errors from this extension on the [BuildInput].
+  /// Reports errors from this extension on the [BuildInput].
   Future<ValidationErrors> validateBuildInput(BuildInput input);
 
-  /// Reports semantic errors from this extension on the [LinkInput].
+  /// Reports errors from this extension on the [LinkInput].
   Future<ValidationErrors> validateBuildOutput(
     BuildInput input,
     BuildOutput output,
   );
 
-  /// Reports semantic errors from this extension on the [LinkInput].
+  /// Reports errors from this extension on the [LinkInput].
   Future<ValidationErrors> validateLinkInput(LinkInput input);
 
-  /// Reports semantic errors from this extension on the [LinkOutput].
+  /// Reports errors from this extension on the [LinkOutput].
   Future<ValidationErrors> validateLinkOutput(
     LinkInput input,
     LinkOutput output,
diff --git a/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart b/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart
index bb2b4492a9..b1820c61ff 100644
--- a/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart
+++ b/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart
@@ -28,6 +28,10 @@ class Asset {
     json.setOrRemove('type', value);
   }
 
+  List<String> _validateType() => _reader.validate<String>('type');
+
+  List<String> validate() => [..._validateType()];
+
   @override
   String toString() => 'Asset($json)';
 }
@@ -54,6 +58,15 @@ class BuildConfig extends Config {
     json.setOrRemove('linking_enabled', value);
   }
 
+  List<String> _validateLinkingEnabled() =>
+      _reader.validate<bool>('linking_enabled');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validateLinkingEnabled(),
+  ];
+
   @override
   String toString() => 'BuildConfig($json)';
 }
@@ -95,6 +108,16 @@ class BuildInput extends HookInput {
     json.setOrRemove('dependency_metadata', value);
   }
 
+  List<String> _validateDependencyMetadata() =>
+      _reader.validateOptionalMap<Map<String, Object?>>('dependency_metadata');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validateConfig(),
+    ..._validateDependencyMetadata(),
+  ];
+
   @override
   String toString() => 'BuildInput($json)';
 }
@@ -157,6 +180,24 @@ class BuildOutput extends HookOutput {
     json.sortOnKey();
   }
 
+  List<String> _validateAssetsForLinking() {
+    final mapErrors = _reader.validateOptionalMap('assetsForLinking');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    final jsonValue = _reader.optionalMap('assetsForLinking');
+    if (jsonValue == null) {
+      return [];
+    }
+    final result = <String>[];
+    for (final list in assetsForLinking!.values) {
+      for (final element in list) {
+        result.addAll(element.validate());
+      }
+    }
+    return result;
+  }
+
   Map<String, Object?>? get metadata => _reader.optionalMap('metadata');
 
   set metadata(Map<String, Object?>? value) {
@@ -164,6 +205,15 @@ class BuildOutput extends HookOutput {
     json.sortOnKey();
   }
 
+  List<String> _validateMetadata() => _reader.validateOptionalMap('metadata');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validateAssetsForLinking(),
+    ..._validateMetadata(),
+  ];
+
   @override
   String toString() => 'BuildOutput($json)';
 }
@@ -189,6 +239,11 @@ class Config {
     json.sortOnKey();
   }
 
+  List<String> _validateBuildAssetTypes() =>
+      _reader.validateStringList('build_asset_types');
+
+  List<String> validate() => [..._validateBuildAssetTypes()];
+
   @override
   String toString() => 'Config($json)';
 }
@@ -232,6 +287,14 @@ class HookInput {
     json.sortOnKey();
   }
 
+  List<String> _validateConfig() {
+    final mapErrors = _reader.validate<Map<String, Object?>>('config');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return config.validate();
+  }
+
   Uri get outDir => _reader.path$('out_dir');
 
   set outDir(Uri value) {
@@ -239,6 +302,8 @@ class HookInput {
     json.sortOnKey();
   }
 
+  List<String> _validateOutDir() => _reader.validatePath('out_dir');
+
   Uri get outDirShared => _reader.path$('out_dir_shared');
 
   set outDirShared(Uri value) {
@@ -246,6 +311,9 @@ class HookInput {
     json.sortOnKey();
   }
 
+  List<String> _validateOutDirShared() =>
+      _reader.validatePath('out_dir_shared');
+
   Uri? get outFile => _reader.optionalPath('out_file');
 
   set outFile(Uri? value) {
@@ -253,6 +321,8 @@ class HookInput {
     json.sortOnKey();
   }
 
+  List<String> _validateOutFile() => _reader.validateOptionalPath('out_file');
+
   String get packageName => _reader.get<String>('package_name');
 
   set packageName(String value) {
@@ -260,6 +330,9 @@ class HookInput {
     json.sortOnKey();
   }
 
+  List<String> _validatePackageName() =>
+      _reader.validate<String>('package_name');
+
   Uri get packageRoot => _reader.path$('package_root');
 
   set packageRoot(Uri value) {
@@ -267,6 +340,8 @@ class HookInput {
     json.sortOnKey();
   }
 
+  List<String> _validatePackageRoot() => _reader.validatePath('package_root');
+
   String get version => _reader.get<String>('version');
 
   set version(String value) {
@@ -274,6 +349,18 @@ class HookInput {
     json.sortOnKey();
   }
 
+  List<String> _validateVersion() => _reader.validate<String>('version');
+
+  List<String> validate() => [
+    ..._validateConfig(),
+    ..._validateOutDir(),
+    ..._validateOutDirShared(),
+    ..._validateOutFile(),
+    ..._validatePackageName(),
+    ..._validatePackageRoot(),
+    ..._validateVersion(),
+  ];
+
   @override
   String toString() => 'HookInput($json)';
 }
@@ -321,6 +408,20 @@ class HookOutput {
     json.sortOnKey();
   }
 
+  List<String> _validateAssets() {
+    final listErrors = _reader.validateOptionalList<Map<String, Object?>>(
+      'assets',
+    );
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    final elements = assets;
+    if (elements == null) {
+      return [];
+    }
+    return [for (final element in elements) ...element.validate()];
+  }
+
   List<Uri>? get dependencies => _reader.optionalPathList('dependencies');
 
   set dependencies(List<Uri>? value) {
@@ -328,6 +429,9 @@ class HookOutput {
     json.sortOnKey();
   }
 
+  List<String> _validateDependencies() =>
+      _reader.validateOptionalPathList('dependencies');
+
   String get timestamp => _reader.get<String>('timestamp');
 
   set timestamp(String value) {
@@ -335,6 +439,8 @@ class HookOutput {
     json.sortOnKey();
   }
 
+  List<String> _validateTimestamp() => _reader.validate<String>('timestamp');
+
   String get version => _reader.get<String>('version');
 
   set version(String value) {
@@ -342,6 +448,15 @@ class HookOutput {
     json.sortOnKey();
   }
 
+  List<String> _validateVersion() => _reader.validate<String>('version');
+
+  List<String> validate() => [
+    ..._validateAssets(),
+    ..._validateDependencies(),
+    ..._validateTimestamp(),
+    ..._validateVersion(),
+  ];
+
   @override
   String toString() => 'HookOutput($json)';
 }
@@ -392,12 +507,36 @@ class LinkInput extends HookInput {
     }
   }
 
+  List<String> _validateAssets() {
+    final listErrors = _reader.validateOptionalList<Map<String, Object?>>(
+      'assets',
+    );
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    final elements = assets;
+    if (elements == null) {
+      return [];
+    }
+    return [for (final element in elements) ...element.validate()];
+  }
+
   Uri? get resourceIdentifiers => _reader.optionalPath('resource_identifiers');
 
   set _resourceIdentifiers(Uri? value) {
     json.setOrRemove('resource_identifiers', value?.toFilePath());
   }
 
+  List<String> _validateResourceIdentifiers() =>
+      _reader.validateOptionalPath('resource_identifiers');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validateAssets(),
+    ..._validateResourceIdentifiers(),
+  ];
+
   @override
   String toString() => 'LinkInput($json)';
 }
@@ -412,6 +551,9 @@ class LinkOutput extends HookOutput {
     required super.version,
   }) : super();
 
+  @override
+  List<String> validate() => [...super.validate()];
+
   @override
   String toString() => 'LinkOutput($json)';
 }
@@ -432,22 +574,46 @@ class JsonReader {
   T get<T extends Object?>(String key) {
     final value = json[key];
     if (value is T) return value;
-    final pathString = _jsonPathToString([key]);
-    if (value == null) {
-      throw FormatException("No value was provided for '$pathString'.");
-    }
     throwFormatException(value, T, [key]);
   }
 
+  List<String> validate<T extends Object?>(String key) {
+    final value = json[key];
+    if (value is T) return [];
+    return [
+      errorString(value, T, [key]),
+    ];
+  }
+
   List<T> list<T extends Object?>(String key) =>
       _castList<T>(get<List<Object?>>(key), key);
 
+  List<String> validateList<T extends Object?>(String key) {
+    final listErrors = validate<List<Object?>>(key);
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    return _validateListElements(get<List<Object?>>(key), key);
+  }
+
   List<T>? optionalList<T extends Object?>(String key) =>
       switch (get<List<Object?>?>(key)?.cast<T>()) {
         null => null,
         final l => _castList<T>(l, key),
       };
 
+  List<String> validateOptionalList<T extends Object?>(String key) {
+    final listErrors = validate<List<Object?>?>(key);
+    if (listErrors.isNotEmpty) {
+      return listErrors;
+    }
+    final list = get<List<Object?>?>(key);
+    if (list == null) {
+      return [];
+    }
+    return _validateListElements(list, key);
+  }
+
   /// [List.cast] but with [FormatException]s.
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
     var index = 0;
@@ -460,6 +626,21 @@ class JsonReader {
     return list.cast();
   }
 
+  List<String> _validateListElements<T extends Object?>(
+    List<Object?> list,
+    String key,
+  ) {
+    var index = 0;
+    final result = <String>[];
+    for (final value in list) {
+      if (value is! T) {
+        result.add(errorString(value, T, [key, index]));
+      }
+      index++;
+    }
+    return result;
+  }
+
   List<T>? optionalListParsed<T extends Object?>(
     String key,
     T Function(Object?) elementParser,
@@ -472,12 +653,32 @@ class JsonReader {
   Map<String, T> map$<T extends Object?>(String key) =>
       _castMap<T>(get<Map<String, Object?>>(key), key);
 
+  List<String> validateMap<T extends Object?>(String key) {
+    final mapErrors = validate<Map<String, Object?>>(key);
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return _validateMapElements<T>(get<Map<String, Object?>>(key), key);
+  }
+
   Map<String, T>? optionalMap<T extends Object?>(String key) =>
       switch (get<Map<String, Object?>?>(key)) {
         null => null,
         final m => _castMap<T>(m, key),
       };
 
+  List<String> validateOptionalMap<T extends Object?>(String key) {
+    final mapErrors = validate<Map<String, Object?>?>(key);
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    final map = get<Map<String, Object?>?>(key);
+    if (map == null) {
+      return [];
+    }
+    return _validateMapElements<T>(map, key);
+  }
+
   /// [Map.cast] but with [FormatException]s.
   Map<String, T> _castMap<T extends Object?>(
     Map<String, Object?> map_,
@@ -491,18 +692,40 @@ class JsonReader {
     return map_.cast();
   }
 
+  List<String> _validateMapElements<T extends Object?>(
+    Map<String, Object?> map_,
+    String parentKey,
+  ) {
+    final result = <String>[];
+    for (final MapEntry(:key, :value) in map_.entries) {
+      if (value is! T) {
+        result.add(errorString(value, T, [parentKey, key]));
+      }
+    }
+    return result;
+  }
+
   List<String>? optionalStringList(String key) => optionalList<String>(key);
 
+  List<String> validateOptionalStringList(String key) =>
+      validateOptionalList<String>(key);
+
   List<String> stringList(String key) => list<String>(key);
 
+  List<String> validateStringList(String key) => validateList<String>(key);
+
   Uri path$(String key) => _fileSystemPathToUri(get<String>(key));
 
+  List<String> validatePath(String key) => validate<String>(key);
+
   Uri? optionalPath(String key) {
     final value = get<String?>(key);
     if (value == null) return null;
     return _fileSystemPathToUri(value);
   }
 
+  List<String> validateOptionalPath(String key) => validate<String?>(key);
+
   List<Uri>? optionalPathList(String key) {
     final strings = optionalStringList(key);
     if (strings == null) {
@@ -511,6 +734,9 @@ class JsonReader {
     return [for (final string in strings) _fileSystemPathToUri(string)];
   }
 
+  List<String> validateOptionalPathList(String key) =>
+      validateOptionalStringList(key);
+
   static Uri _fileSystemPathToUri(String path) {
     if (path.endsWith(Platform.pathSeparator)) {
       return Uri.directory(path);
@@ -525,12 +751,22 @@ class JsonReader {
     Object? value,
     Type expectedType,
     List<Object> pathExtension,
+  ) {
+    throw FormatException(errorString(value, expectedType, pathExtension));
+  }
+
+  String errorString(
+    Object? value,
+    Type expectedType,
+    List<Object> pathExtension,
   ) {
     final pathString = _jsonPathToString(pathExtension);
-    throw FormatException(
-      "Unexpected value '$value' (${value.runtimeType}) for '$pathString'. "
-      'Expected a $expectedType.',
-    );
+    if (value == null) {
+      return "No value was provided for '$pathString'."
+          ' Expected a $expectedType.';
+    }
+    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+        ' Expected a $expectedType.';
   }
 }
 
diff --git a/pkgs/native_assets_cli/lib/src/validation.dart b/pkgs/native_assets_cli/lib/src/validation.dart
index bec1b1915c..d0f2fd8cb3 100644
--- a/pkgs/native_assets_cli/lib/src/validation.dart
+++ b/pkgs/native_assets_cli/lib/src/validation.dart
@@ -5,13 +5,25 @@
 import 'dart:io';
 
 import '../native_assets_cli_builder.dart';
+import 'hook/syntax.g.dart' as syntax;
 
 typedef ValidationErrors = List<String>;
 
-Future<ValidationErrors> validateBuildInput(BuildInput input) async =>
-    _validateHookInput('BuildInput', input);
+Future<ValidationErrors> validateBuildInput(BuildInput input) async {
+  final syntaxErrors = syntax.BuildInput.fromJson(input.json).validate();
+  if (syntaxErrors.isNotEmpty) {
+    return [...syntaxErrors, _semanticValidationSkippedMessage];
+  }
+
+  return _validateHookInput('BuildInput', input);
+}
 
 Future<ValidationErrors> validateLinkInput(LinkInput input) async {
+  final syntaxErrors = syntax.LinkInput.fromJson(input.json).validate();
+  if (syntaxErrors.isNotEmpty) {
+    return [...syntaxErrors, _semanticValidationSkippedMessage];
+  }
+
   final recordUses = input.recordedUsagesFile;
   return <String>[
     ..._validateHookInput('LinkInput', input),
@@ -61,6 +73,11 @@ Future<ValidationErrors> validateBuildOutput(
   BuildInput input,
   BuildOutput output,
 ) async {
+  final syntaxErrors = syntax.BuildOutput.fromJson(output.json).validate();
+  if (syntaxErrors.isNotEmpty) {
+    return [...syntaxErrors, _semanticValidationSkippedMessage];
+  }
+
   final errors = [
     ..._validateAssetsForLinking(input, output),
     ..._validateOutputAssetTypes(input, output.assets.encodedAssets),
@@ -78,6 +95,11 @@ Future<ValidationErrors> validateLinkOutput(
   LinkInput input,
   LinkOutput output,
 ) async {
+  final syntaxErrors = syntax.LinkOutput.fromJson(output.json).validate();
+  if (syntaxErrors.isNotEmpty) {
+    return [...syntaxErrors, _semanticValidationSkippedMessage];
+  }
+
   final errors = [
     ..._validateOutputAssetTypes(input, output.assets.encodedAssets),
   ];
@@ -121,6 +143,9 @@ List<String> _validateAssetsForLinking(BuildInput input, BuildOutput output) {
   return errors;
 }
 
+const _semanticValidationSkippedMessage =
+    'Syntax errors. Semantic validation skipped.';
+
 class ValidationFailure implements Exception {
   final String? message;
 

From 30913fe791e8020567f3f8b83ebc123ca5bad8ee Mon Sep 17 00:00:00 2001
From: Daco Harkes <dacoharkes@google.com>
Date: Thu, 20 Mar 2025 14:04:46 +0100
Subject: [PATCH 2/2] address comments

---
 .../lib/src/generator/helper_library.dart     | 17 +-----
 .../lib/src/generator/property_generator.dart | 22 ++++----
 .../lib/src/code_assets/syntax.g.dart         | 17 +-----
 .../lib/src/code_assets/validation.dart       | 10 ++--
 .../lib/src/data_assets/syntax.g.dart         | 17 +-----
 .../lib/src/data_assets/validation.dart       |  4 +-
 .../lib/src/hook/syntax.g.dart                | 56 ++++++++-----------
 .../native_assets_cli/lib/src/validation.dart |  7 ++-
 8 files changed, 51 insertions(+), 99 deletions(-)

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 91857c6b70..e93a71bffb 100644
--- a/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart
+++ b/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart
@@ -64,12 +64,10 @@ class JsonReader {
 
   /// [List.cast] but with [FormatException]s.
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
-    var index = 0;
-    for (final value in list) {
+    for (final (index, value) in list.indexed) {
       if (value is! T) {
         throwFormatException(value, T, [key, index]);
       }
-      index++;
     }
     return list.cast();
   }
@@ -78,26 +76,15 @@ class JsonReader {
     List<Object?> list,
     String key,
   ) {
-    var index = 0;
     final result = <String>[];
-    for (final value in list) {
+    for (final (index, value) in list.indexed) {
       if (value is! T) {
         result.add(errorString(value, T, [key, index]));
       }
-      index++;
     }
     return result;
   }
 
-  List<T>? optionalListParsed<T extends Object?>(
-    String key,
-    T Function(Object?) elementParser,
-  ) {
-    final jsonValue = optionalList(key);
-    if (jsonValue == null) return null;
-    return [for (final element in jsonValue) elementParser(element)];
-  }
-
   Map<String, T> map$<T extends Object?>(String key) =>
       _castMap<T>(get<Map<String, Object?>>(key), key);
 
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 093838d88f..f12daf3dd3 100644
--- a/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart
+++ b/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart
@@ -218,12 +218,11 @@ $dartType get $fieldName {
   }
   final result = <String, List<Asset>>{};
   for (final MapEntry(:key, :value) in jsonValue.entries) {
-    var index = 0;
     result[key] = [
-      for (final item in value as List<Object?>)
+      for (final (index, item) in (value as List<Object?>).indexed)
         $typeName.fromJson(
           item as $jsonObjectDartType,
-          path: [...path, key, index++],
+          path: [...path, key, index],
         ),
     ];
   }
@@ -311,14 +310,15 @@ List<String> $validateName() => _reader.validateOptionalMap('$jsonKey');
         }
         buffer.writeln('''
 $dartType get $fieldName {
-  var index = 0;
-  return _reader.optionalListParsed(
-    '$jsonKey',
-    (e) => $typeName.fromJson(
-      e as Map<String, Object?>,
-      path: [...path, '$jsonKey', index++],
-    ),
-  );
+  final jsonValue = _reader.optionalList('$jsonKey');
+  if (jsonValue == null) return null;
+  return [
+    for (final (index, element) in jsonValue.indexed)
+      $typeName.fromJson(
+        element as Map<String, Object?>,
+        path: [...path, '$jsonKey', index],
+      ),
+  ];
 }
 
 set $setterName($dartType value) {
diff --git a/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart b/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart
index ff06c586ff..b81e9e86f7 100644
--- a/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart
+++ b/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart
@@ -953,12 +953,10 @@ class JsonReader {
 
   /// [List.cast] but with [FormatException]s.
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
-    var index = 0;
-    for (final value in list) {
+    for (final (index, value) in list.indexed) {
       if (value is! T) {
         throwFormatException(value, T, [key, index]);
       }
-      index++;
     }
     return list.cast();
   }
@@ -967,26 +965,15 @@ class JsonReader {
     List<Object?> list,
     String key,
   ) {
-    var index = 0;
     final result = <String>[];
-    for (final value in list) {
+    for (final (index, value) in list.indexed) {
       if (value is! T) {
         result.add(errorString(value, T, [key, index]));
       }
-      index++;
     }
     return result;
   }
 
-  List<T>? optionalListParsed<T extends Object?>(
-    String key,
-    T Function(Object?) elementParser,
-  ) {
-    final jsonValue = optionalList(key);
-    if (jsonValue == null) return null;
-    return [for (final element in jsonValue) elementParser(element)];
-  }
-
   Map<String, T> map$<T extends Object?>(String key) =>
       _castMap<T>(get<Map<String, Object?>>(key), key);
 
diff --git a/pkgs/native_assets_cli/lib/src/code_assets/validation.dart b/pkgs/native_assets_cli/lib/src/code_assets/validation.dart
index d68dcea6a5..7a19e24b16 100644
--- a/pkgs/native_assets_cli/lib/src/code_assets/validation.dart
+++ b/pkgs/native_assets_cli/lib/src/code_assets/validation.dart
@@ -78,7 +78,7 @@ ValidationErrors _validateConfig(String inputName, HookConfig config) {
   return errors;
 }
 
-List<String> _validateConfigSyntax(HookConfig config) {
+ValidationErrors _validateConfigSyntax(HookConfig config) {
   final syntaxNode = syntax.Config.fromJson(config.json, path: config.path);
   final syntaxErrors = syntaxNode.validate();
   if (syntaxErrors.isEmpty) {
@@ -206,7 +206,7 @@ Future<ValidationErrors> _validateCodeAssetBuildOrLinkOutput(
   return errors;
 }
 
-List<String> _validateCodeAssetSyntax(EncodedAsset encodedAsset) {
+ValidationErrors _validateCodeAssetSyntax(EncodedAsset encodedAsset) {
   final syntaxNode = syntax.Asset.fromJson(
     encodedAsset.toJson(),
     path: encodedAsset.jsonPath ?? [],
@@ -230,7 +230,7 @@ void _validateCodeAsset(
   HookInput input,
   CodeConfig codeConfig,
   CodeAsset codeAsset,
-  List<String> errors,
+  ValidationErrors errors,
   Set<String> ids,
   bool validateAssetId,
   bool validateLinkMode,
@@ -281,7 +281,7 @@ void _validateCodeAsset(
   errors.addAll(_validateCodeAssetFile(codeAsset));
 }
 
-List<String> _validateCodeAssetFile(CodeAsset codeAsset) {
+ValidationErrors _validateCodeAssetFile(CodeAsset codeAsset) {
   final id = codeAsset.id;
   final file = codeAsset.file;
   return [
@@ -313,7 +313,7 @@ void _groupCodeAssetsByFilename(
 }
 
 void _validateNoDuplicateDylibNames(
-  List<String> errors,
+  ValidationErrors errors,
   Map<String, Set<String>> fileNameToEncodedAssetId,
 ) {
   for (final fileName in fileNameToEncodedAssetId.keys) {
diff --git a/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart b/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart
index 34c1649c17..6d028093d8 100644
--- a/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart
+++ b/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart
@@ -160,12 +160,10 @@ class JsonReader {
 
   /// [List.cast] but with [FormatException]s.
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
-    var index = 0;
-    for (final value in list) {
+    for (final (index, value) in list.indexed) {
       if (value is! T) {
         throwFormatException(value, T, [key, index]);
       }
-      index++;
     }
     return list.cast();
   }
@@ -174,26 +172,15 @@ class JsonReader {
     List<Object?> list,
     String key,
   ) {
-    var index = 0;
     final result = <String>[];
-    for (final value in list) {
+    for (final (index, value) in list.indexed) {
       if (value is! T) {
         result.add(errorString(value, T, [key, index]));
       }
-      index++;
     }
     return result;
   }
 
-  List<T>? optionalListParsed<T extends Object?>(
-    String key,
-    T Function(Object?) elementParser,
-  ) {
-    final jsonValue = optionalList(key);
-    if (jsonValue == null) return null;
-    return [for (final element in jsonValue) elementParser(element)];
-  }
-
   Map<String, T> map$<T extends Object?>(String key) =>
       _castMap<T>(get<Map<String, Object?>>(key), key);
 
diff --git a/pkgs/native_assets_cli/lib/src/data_assets/validation.dart b/pkgs/native_assets_cli/lib/src/data_assets/validation.dart
index dc6801a55e..0b88dc6117 100644
--- a/pkgs/native_assets_cli/lib/src/data_assets/validation.dart
+++ b/pkgs/native_assets_cli/lib/src/data_assets/validation.dart
@@ -77,7 +77,7 @@ Future<ValidationErrors> _validateDataAssetBuildOrLinkOutput(
 void _validateDataAsset(
   HookInput input,
   DataAsset dataAsset,
-  List<String> errors,
+  ValidationErrors errors,
   Set<String> ids,
   bool isBuild,
 ) {
@@ -91,7 +91,7 @@ void _validateDataAsset(
   errors.addAll(_validateFile('Data asset ${dataAsset.name} file', file));
 }
 
-List<String> _validateDataAssetSyntax(EncodedAsset encodedAsset) {
+ValidationErrors _validateDataAssetSyntax(EncodedAsset encodedAsset) {
   final syntaxNode = syntax.Asset.fromJson(
     encodedAsset.toJson(),
     path: encodedAsset.jsonPath ?? [],
diff --git a/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart b/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart
index b1820c61ff..335f574d81 100644
--- a/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart
+++ b/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart
@@ -156,12 +156,11 @@ class BuildOutput extends HookOutput {
     }
     final result = <String, List<Asset>>{};
     for (final MapEntry(:key, :value) in jsonValue.entries) {
-      var index = 0;
       result[key] = [
-        for (final item in value as List<Object?>)
+        for (final (index, item) in (value as List<Object?>).indexed)
           Asset.fromJson(
             item as Map<String, Object?>,
-            path: [...path, key, index++],
+            path: [...path, key, index],
           ),
       ];
     }
@@ -389,14 +388,15 @@ class HookOutput {
   }
 
   List<Asset>? get assets {
-    var index = 0;
-    return _reader.optionalListParsed(
-      'assets',
-      (e) => Asset.fromJson(
-        e as Map<String, Object?>,
-        path: [...path, 'assets', index++],
-      ),
-    );
+    final jsonValue = _reader.optionalList('assets');
+    if (jsonValue == null) return null;
+    return [
+      for (final (index, element) in jsonValue.indexed)
+        Asset.fromJson(
+          element as Map<String, Object?>,
+          path: [...path, 'assets', index],
+        ),
+    ];
   }
 
   set assets(List<Asset>? value) {
@@ -489,14 +489,15 @@ class LinkInput extends HookInput {
   }
 
   List<Asset>? get assets {
-    var index = 0;
-    return _reader.optionalListParsed(
-      'assets',
-      (e) => Asset.fromJson(
-        e as Map<String, Object?>,
-        path: [...path, 'assets', index++],
-      ),
-    );
+    final jsonValue = _reader.optionalList('assets');
+    if (jsonValue == null) return null;
+    return [
+      for (final (index, element) in jsonValue.indexed)
+        Asset.fromJson(
+          element as Map<String, Object?>,
+          path: [...path, 'assets', index],
+        ),
+    ];
   }
 
   set _assets(List<Asset>? value) {
@@ -616,12 +617,10 @@ class JsonReader {
 
   /// [List.cast] but with [FormatException]s.
   List<T> _castList<T extends Object?>(List<Object?> list, String key) {
-    var index = 0;
-    for (final value in list) {
+    for (final (index, value) in list.indexed) {
       if (value is! T) {
         throwFormatException(value, T, [key, index]);
       }
-      index++;
     }
     return list.cast();
   }
@@ -630,26 +629,15 @@ class JsonReader {
     List<Object?> list,
     String key,
   ) {
-    var index = 0;
     final result = <String>[];
-    for (final value in list) {
+    for (final (index, value) in list.indexed) {
       if (value is! T) {
         result.add(errorString(value, T, [key, index]));
       }
-      index++;
     }
     return result;
   }
 
-  List<T>? optionalListParsed<T extends Object?>(
-    String key,
-    T Function(Object?) elementParser,
-  ) {
-    final jsonValue = optionalList(key);
-    if (jsonValue == null) return null;
-    return [for (final element in jsonValue) elementParser(element)];
-  }
-
   Map<String, T> map$<T extends Object?>(String key) =>
       _castMap<T>(get<Map<String, Object?>>(key), key);
 
diff --git a/pkgs/native_assets_cli/lib/src/validation.dart b/pkgs/native_assets_cli/lib/src/validation.dart
index d0f2fd8cb3..1c798ffee7 100644
--- a/pkgs/native_assets_cli/lib/src/validation.dart
+++ b/pkgs/native_assets_cli/lib/src/validation.dart
@@ -107,7 +107,7 @@ Future<ValidationErrors> validateLinkOutput(
 }
 
 /// Only output asset types that are supported by the embedder.
-List<String> _validateOutputAssetTypes(
+ValidationErrors _validateOutputAssetTypes(
   HookInput input,
   Iterable<EncodedAsset> assets,
 ) {
@@ -130,7 +130,10 @@ List<String> _validateOutputAssetTypes(
 }
 
 /// EncodedAssetsForLinking should be empty if linking is not supported.
-List<String> _validateAssetsForLinking(BuildInput input, BuildOutput output) {
+ValidationErrors _validateAssetsForLinking(
+  BuildInput input,
+  BuildOutput output,
+) {
   final errors = <String>[];
   if (!input.config.linkingEnabled) {
     if (output.assets.encodedAssetsForLinking.isNotEmpty) {