[pub_formats] Generate `pubspec.lock` API (#2427)

diff --git a/.github/pr-title-checker-config.json b/.github/pr-title-checker-config.json
index a1b2f1c..d80ed7a 100644
--- a/.github/pr-title-checker-config.json
+++ b/.github/pr-title-checker-config.json
@@ -15,6 +15,7 @@
             "[native_test_helpers] ",
             "[native_toolchain_c] ",
             "[objective_c] ",
+            "[pub_formats] ",
             "[swift2objc] ",
             "[swiftgen] "
         ],
diff --git a/pkgs/pub_formats/doc/schema/pubspec_lock.schema.json b/pkgs/pub_formats/doc/schema/pubspec_lock.schema.json
new file mode 100644
index 0000000..0b179f6
--- /dev/null
+++ b/pkgs/pub_formats/doc/schema/pubspec_lock.schema.json
@@ -0,0 +1,196 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "Pubspec Lock File Schema",
+    "description": "Schema for a Dart pubspec.lock file. Note that this is reverse engineered and not the source of truth for pubspec locks.",
+    "$ref": "#/definitions/PubspecLockFile",
+    "definitions": {
+        "PubspecLockFile": {
+            "type": "object",
+            "required": [
+                "sdks"
+            ],
+            "properties": {
+                "packages": {
+                    "type": "object",
+                    "description": "Details of the locked packages.",
+                    "additionalProperties": {
+                        "$ref": "#/definitions/Package"
+                    }
+                },
+                "sdks": {
+                    "$ref": "#/definitions/SDKs"
+                }
+            },
+            "additionalProperties": false
+        },
+        "Package": {
+            "type": "object",
+            "required": [
+                "dependency",
+                "description",
+                "source",
+                "version"
+            ],
+            "properties": {
+                "dependency": {
+                    "$ref": "#/definitions/DependencyType"
+                },
+                "description": {
+                    "$ref": "#/definitions/PackageDescription"
+                },
+                "source": {
+                    "$ref": "#/definitions/PackageSource"
+                },
+                "version": {
+                    "$ref": "#/definitions/PackageVersion"
+                }
+            },
+            "additionalProperties": false
+        },
+        "DependencyType": {
+            "type": "string",
+            "description": "The type of dependency.",
+            "anyOf": [
+                {
+                    "enum": [
+                        "transitive",
+                        "direct main"
+                    ]
+                },
+                {
+                    "type": "string"
+                }
+            ]
+        },
+        "PackageDescription": {
+            "type": "object",
+            "description": "Description of the package source."
+        },
+        "HostedPackageDescription": {
+            "description": "For hosted packages.",
+            "type": "object",
+            "allOf": [
+                {
+                    "$ref": "#/definitions/PackageDescription"
+                }
+            ],
+            "required": [
+                "name",
+                "sha256",
+                "url"
+            ],
+            "properties": {
+                "name": {
+                    "type": "string",
+                    "description": "Name of the package."
+                },
+                "sha256": {
+                    "type": "string",
+                    "description": "SHA256 checksum of the package."
+                },
+                "url": {
+                    "type": "string",
+                    "format": "uri",
+                    "description": "URL of the package host."
+                }
+            },
+            "additionalProperties": false
+        },
+        "GitPackageDescription": {
+            "description": "For git packages.",
+            "type": "object",
+            "allOf": [
+                {
+                    "$ref": "#/definitions/PackageDescription"
+                }
+            ],
+            "required": [
+                "path",
+                "ref",
+                "resolved-ref",
+                "url"
+            ],
+            "properties": {
+                "path": {
+                    "type": "string",
+                    "description": "Path within the git repository (if applicable)."
+                },
+                "ref": {
+                    "type": "string",
+                    "description": "Git reference (e.g., branch, tag, or commit hash)."
+                },
+                "resolved-ref": {
+                    "type": "string",
+                    "description": "Resolved git commit hash."
+                },
+                "url": {
+                    "type": "string",
+                    "format": "uri",
+                    "description": "URL of the git repository."
+                }
+            },
+            "additionalProperties": false
+        },
+        "PathPackageDescription": {
+            "description": "For path packages.",
+            "type": "object",
+            "allOf": [
+                {
+                    "$ref": "#/definitions/PackageDescription"
+                }
+            ],
+            "required": [
+                "path",
+                "relative"
+            ],
+            "properties": {
+                "path": {
+                    "type": "string",
+                    "description": "Absolute or relative path to the package."
+                },
+                "relative": {
+                    "type": "boolean",
+                    "description": "Indicates if the path is relative to the lockfile."
+                }
+            },
+            "additionalProperties": false
+        },
+        "PackageSource": {
+            "type": "string",
+            "description": "The source of the package.",
+            "anyOf": [
+                {
+                    "enum": [
+                        "hosted",
+                        "git",
+                        "path"
+                    ]
+                },
+                {
+                    "type": "string"
+                }
+            ]
+        },
+        "PackageVersion": {
+            "type": "string",
+            "description": "The locked version of the package."
+        },
+        "SDKs": {
+            "type": "object",
+            "description": "Details of the SDKs used.",
+            "required": [
+                "dart"
+            ],
+            "properties": {
+                "dart": {
+                    "$ref": "#/definitions/DartSDKVersion"
+                }
+            },
+            "additionalProperties": false
+        },
+        "DartSDKVersion": {
+            "type": "string",
+            "description": "The Dart SDK version constraint."
+        }
+    }
+}
diff --git a/pkgs/pub_formats/lib/pubspec_formats.dart b/pkgs/pub_formats/lib/pubspec_formats.dart
new file mode 100644
index 0000000..2f5926c
--- /dev/null
+++ b/pkgs/pub_formats/lib/pubspec_formats.dart
@@ -0,0 +1,11 @@
+// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// An internal library containing file formats for the pub client. This library
+/// is not meant to have a stable API and might break at any point when
+/// `package:json_syntax_generator` is updated. This API is used in the Dart
+/// SDK, so when doing breaking changes please roll ASAP to the Dart SDK.
+library;
+
+export 'src/pubspec_lock_syntax.g.dart';
diff --git a/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart b/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart
new file mode 100644
index 0000000..b99fb4b
--- /dev/null
+++ b/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart
@@ -0,0 +1,712 @@
+// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This file is generated, do not edit.
+// File generated by pkgs/pub_formats/tool/generate.dart.
+// Must be rerun when pkgs/pub_formats/doc/schema/ is modified.
+
+// ignore_for_file: unused_element, public_member_api_docs
+
+import 'dart:io';
+
+class DependencyTypeSyntax {
+  final String name;
+
+  const DependencyTypeSyntax._(this.name);
+
+  static const directMain = DependencyTypeSyntax._('direct main');
+
+  static const transitive = DependencyTypeSyntax._('transitive');
+
+  static const List<DependencyTypeSyntax> values = [directMain, transitive];
+
+  static final Map<String, DependencyTypeSyntax> _byName = {
+    for (final value in values) value.name: value,
+  };
+
+  DependencyTypeSyntax.unknown(this.name)
+    : assert(!_byName.keys.contains(name));
+
+  factory DependencyTypeSyntax.fromJson(String name) {
+    final knownValue = _byName[name];
+    if (knownValue != null) {
+      return knownValue;
+    }
+    return DependencyTypeSyntax.unknown(name);
+  }
+
+  bool get isKnown => _byName[name] != null;
+
+  @override
+  String toString() => name;
+}
+
+class GitPackageDescriptionSyntax extends PackageDescriptionSyntax {
+  GitPackageDescriptionSyntax.fromJson(super.json, {super.path})
+    : super.fromJson();
+
+  GitPackageDescriptionSyntax({
+    required String path$,
+    required String ref,
+    required String resolvedRef,
+    required String url,
+  }) : super() {
+    _path$ = path$;
+    _ref = ref;
+    _resolvedRef = resolvedRef;
+    _url = url;
+    json.sortOnKey();
+  }
+
+  /// Setup all fields for [GitPackageDescriptionSyntax] that are not in
+  /// [PackageDescriptionSyntax].
+  void setup({
+    required String path$,
+    required String ref,
+    required String resolvedRef,
+    required String url,
+  }) {
+    _path$ = path$;
+    _ref = ref;
+    _resolvedRef = resolvedRef;
+    _url = url;
+    json.sortOnKey();
+  }
+
+  String get path$ => _reader.get<String>('path');
+
+  set _path$(String value) {
+    json.setOrRemove('path', value);
+  }
+
+  List<String> _validatePath$() => _reader.validate<String>('path');
+
+  String get ref => _reader.get<String>('ref');
+
+  set _ref(String value) {
+    json.setOrRemove('ref', value);
+  }
+
+  List<String> _validateRef() => _reader.validate<String>('ref');
+
+  String get resolvedRef => _reader.get<String>('resolved-ref');
+
+  set _resolvedRef(String value) {
+    json.setOrRemove('resolved-ref', value);
+  }
+
+  List<String> _validateResolvedRef() =>
+      _reader.validate<String>('resolved-ref');
+
+  String get url => _reader.get<String>('url');
+
+  set _url(String value) {
+    json.setOrRemove('url', value);
+  }
+
+  List<String> _validateUrl() => _reader.validate<String>('url');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validatePath$(),
+    ..._validateRef(),
+    ..._validateResolvedRef(),
+    ..._validateUrl(),
+  ];
+
+  @override
+  String toString() => 'GitPackageDescriptionSyntax($json)';
+}
+
+class HostedPackageDescriptionSyntax extends PackageDescriptionSyntax {
+  HostedPackageDescriptionSyntax.fromJson(super.json, {super.path})
+    : super.fromJson();
+
+  HostedPackageDescriptionSyntax({
+    required String name,
+    required String sha256,
+    required String url,
+  }) : super() {
+    _name = name;
+    _sha256 = sha256;
+    _url = url;
+    json.sortOnKey();
+  }
+
+  /// Setup all fields for [HostedPackageDescriptionSyntax] that are not in
+  /// [PackageDescriptionSyntax].
+  void setup({
+    required String name,
+    required String sha256,
+    required String url,
+  }) {
+    _name = name;
+    _sha256 = sha256;
+    _url = url;
+    json.sortOnKey();
+  }
+
+  String get name => _reader.get<String>('name');
+
+  set _name(String value) {
+    json.setOrRemove('name', value);
+  }
+
+  List<String> _validateName() => _reader.validate<String>('name');
+
+  String get sha256 => _reader.get<String>('sha256');
+
+  set _sha256(String value) {
+    json.setOrRemove('sha256', value);
+  }
+
+  List<String> _validateSha256() => _reader.validate<String>('sha256');
+
+  String get url => _reader.get<String>('url');
+
+  set _url(String value) {
+    json.setOrRemove('url', value);
+  }
+
+  List<String> _validateUrl() => _reader.validate<String>('url');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validateName(),
+    ..._validateSha256(),
+    ..._validateUrl(),
+  ];
+
+  @override
+  String toString() => 'HostedPackageDescriptionSyntax($json)';
+}
+
+class PackageSyntax extends JsonObjectSyntax {
+  PackageSyntax.fromJson(super.json, {super.path = const []})
+    : super.fromJson();
+
+  PackageSyntax({
+    required DependencyTypeSyntax dependency,
+    required PackageDescriptionSyntax description,
+    required PackageSourceSyntax source,
+    required String version,
+  }) : super() {
+    _dependency = dependency;
+    _description = description;
+    _source = source;
+    _version = version;
+    json.sortOnKey();
+  }
+
+  DependencyTypeSyntax get dependency {
+    final jsonValue = _reader.get<String>('dependency');
+    return DependencyTypeSyntax.fromJson(jsonValue);
+  }
+
+  set _dependency(DependencyTypeSyntax value) {
+    json['dependency'] = value.name;
+  }
+
+  List<String> _validateDependency() => _reader.validate<String>('dependency');
+
+  PackageDescriptionSyntax get description {
+    final jsonValue = _reader.map$('description');
+    return PackageDescriptionSyntax.fromJson(
+      jsonValue,
+      path: [...path, 'description'],
+    );
+  }
+
+  set _description(PackageDescriptionSyntax value) {
+    json['description'] = value.json;
+  }
+
+  List<String> _validateDescription() {
+    final mapErrors = _reader.validate<Map<String, Object?>>('description');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return description.validate();
+  }
+
+  PackageSourceSyntax get source {
+    final jsonValue = _reader.get<String>('source');
+    return PackageSourceSyntax.fromJson(jsonValue);
+  }
+
+  set _source(PackageSourceSyntax value) {
+    json['source'] = value.name;
+  }
+
+  List<String> _validateSource() => _reader.validate<String>('source');
+
+  String get version => _reader.get<String>('version');
+
+  set _version(String value) {
+    json.setOrRemove('version', value);
+  }
+
+  List<String> _validateVersion() => _reader.validate<String>('version');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validateDependency(),
+    ..._validateDescription(),
+    ..._validateSource(),
+    ..._validateVersion(),
+  ];
+
+  @override
+  String toString() => 'PackageSyntax($json)';
+}
+
+class PackageDescriptionSyntax extends JsonObjectSyntax {
+  PackageDescriptionSyntax.fromJson(super.json, {super.path = const []})
+    : super.fromJson();
+
+  PackageDescriptionSyntax() : super();
+
+  @override
+  List<String> validate() => [...super.validate()];
+
+  @override
+  String toString() => 'PackageDescriptionSyntax($json)';
+}
+
+class PackageSourceSyntax {
+  final String name;
+
+  const PackageSourceSyntax._(this.name);
+
+  static const git = PackageSourceSyntax._('git');
+
+  static const hosted = PackageSourceSyntax._('hosted');
+
+  static const path$ = PackageSourceSyntax._('path');
+
+  static const List<PackageSourceSyntax> values = [git, hosted, path$];
+
+  static final Map<String, PackageSourceSyntax> _byName = {
+    for (final value in values) value.name: value,
+  };
+
+  PackageSourceSyntax.unknown(this.name) : assert(!_byName.keys.contains(name));
+
+  factory PackageSourceSyntax.fromJson(String name) {
+    final knownValue = _byName[name];
+    if (knownValue != null) {
+      return knownValue;
+    }
+    return PackageSourceSyntax.unknown(name);
+  }
+
+  bool get isKnown => _byName[name] != null;
+
+  @override
+  String toString() => name;
+}
+
+class PathPackageDescriptionSyntax extends PackageDescriptionSyntax {
+  PathPackageDescriptionSyntax.fromJson(super.json, {super.path})
+    : super.fromJson();
+
+  PathPackageDescriptionSyntax({required String path$, required bool relative})
+    : super() {
+    _path$ = path$;
+    _relative = relative;
+    json.sortOnKey();
+  }
+
+  /// Setup all fields for [PathPackageDescriptionSyntax] that are not in
+  /// [PackageDescriptionSyntax].
+  void setup({required String path$, required bool relative}) {
+    _path$ = path$;
+    _relative = relative;
+    json.sortOnKey();
+  }
+
+  String get path$ => _reader.get<String>('path');
+
+  set _path$(String value) {
+    json.setOrRemove('path', value);
+  }
+
+  List<String> _validatePath$() => _reader.validate<String>('path');
+
+  bool get relative => _reader.get<bool>('relative');
+
+  set _relative(bool value) {
+    json.setOrRemove('relative', value);
+  }
+
+  List<String> _validateRelative() => _reader.validate<bool>('relative');
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validatePath$(),
+    ..._validateRelative(),
+  ];
+
+  @override
+  String toString() => 'PathPackageDescriptionSyntax($json)';
+}
+
+class PubspecLockFileSyntax extends JsonObjectSyntax {
+  PubspecLockFileSyntax.fromJson(super.json, {super.path = const []})
+    : super.fromJson();
+
+  PubspecLockFileSyntax({
+    required Map<String, PackageSyntax>? packages,
+    required SDKsSyntax sdks,
+  }) : super() {
+    _packages = packages;
+    _sdks = sdks;
+    json.sortOnKey();
+  }
+
+  Map<String, PackageSyntax>? get packages {
+    final jsonValue = _reader.optionalMap('packages');
+    if (jsonValue == null) {
+      return null;
+    }
+    return {
+      for (final MapEntry(:key, :value) in jsonValue.entries)
+        key: PackageSyntax.fromJson(
+          value as Map<String, Object?>,
+          path: [...path, key],
+        ),
+    };
+  }
+
+  set _packages(Map<String, PackageSyntax>? value) {
+    if (value == null) {
+      json.remove('packages');
+    } else {
+      json['packages'] = {
+        for (final MapEntry(:key, :value) in value.entries) key: value.json,
+      };
+    }
+  }
+
+  List<String> _validatePackages() {
+    final mapErrors = _reader.validateOptionalMap('packages');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    final jsonValue = _reader.optionalMap('packages');
+    if (jsonValue == null) {
+      return [];
+    }
+    final result = <String>[];
+    for (final value in packages!.values) {
+      result.addAll(value.validate());
+    }
+    return result;
+  }
+
+  SDKsSyntax get sdks {
+    final jsonValue = _reader.map$('sdks');
+    return SDKsSyntax.fromJson(jsonValue, path: [...path, 'sdks']);
+  }
+
+  set _sdks(SDKsSyntax value) {
+    json['sdks'] = value.json;
+  }
+
+  List<String> _validateSdks() {
+    final mapErrors = _reader.validate<Map<String, Object?>>('sdks');
+    if (mapErrors.isNotEmpty) {
+      return mapErrors;
+    }
+    return sdks.validate();
+  }
+
+  @override
+  List<String> validate() => [
+    ...super.validate(),
+    ..._validatePackages(),
+    ..._validateSdks(),
+  ];
+
+  @override
+  String toString() => 'PubspecLockFileSyntax($json)';
+}
+
+class SDKsSyntax extends JsonObjectSyntax {
+  SDKsSyntax.fromJson(super.json, {super.path = const []}) : super.fromJson();
+
+  SDKsSyntax({required String dart}) : super() {
+    _dart = dart;
+    json.sortOnKey();
+  }
+
+  String get dart => _reader.get<String>('dart');
+
+  set _dart(String value) {
+    json.setOrRemove('dart', value);
+  }
+
+  List<String> _validateDart() => _reader.validate<String>('dart');
+
+  @override
+  List<String> validate() => [...super.validate(), ..._validateDart()];
+
+  @override
+  String toString() => 'SDKsSyntax($json)';
+}
+
+class JsonObjectSyntax {
+  final Map<String, Object?> json;
+
+  final List<Object> path;
+
+  JsonReader get _reader => JsonReader(json, path);
+
+  JsonObjectSyntax() : json = {}, path = const [];
+
+  JsonObjectSyntax.fromJson(this.json, {this.path = const []});
+
+  List<String> validate() => [];
+}
+
+class JsonReader {
+  /// The JSON Object this reader is reading.
+  final Map<String, Object?> json;
+
+  /// The path traversed by readers of the surrounding JSON.
+  ///
+  /// Contains [String] property keys and [int] indices.
+  ///
+  /// This is used to give more precise error messages.
+  final List<Object> path;
+
+  JsonReader(this.json, this.path);
+
+  T get<T extends Object?>(String key) {
+    final value = json[key];
+    if (value is T) return value;
+    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) {
+    for (final (index, value) in list.indexed) {
+      if (value is! T) {
+        throwFormatException(value, T, [key, index]);
+      }
+    }
+    return list.cast();
+  }
+
+  List<String> _validateListElements<T extends Object?>(
+    List<Object?> list,
+    String key,
+  ) {
+    final result = <String>[];
+    for (final (index, value) in list.indexed) {
+      if (value is! T) {
+        result.add(errorString(value, T, [key, index]));
+      }
+    }
+    return result;
+  }
+
+  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_,
+    String parentKey,
+  ) {
+    for (final MapEntry(:key, :value) in map_.entries) {
+      if (value is! T) {
+        throwFormatException(value, T, [parentKey, key]);
+      }
+    }
+    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) {
+      return null;
+    }
+    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);
+    }
+    return Uri.file(path);
+  }
+
+  String _jsonPathToString(List<Object> pathEnding) =>
+      [...path, ...pathEnding].join('.');
+
+  Never throwFormatException(
+    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);
+    if (value == null) {
+      return "No value was provided for '$pathString'."
+          ' Expected a $expectedType.';
+    }
+    return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+        ' Expected a $expectedType.';
+  }
+
+  /// Traverses a JSON path, returns `null` if the path cannot be traversed.
+  Object? tryTraverse(List<String> path) {
+    Object? json = this.json;
+    for (final key in path) {
+      if (json is! Map<String, Object?>) {
+        return null;
+      }
+      json = json[key];
+    }
+    return json;
+  }
+}
+
+extension on Map<String, Object?> {
+  void setOrRemove(String key, Object? value) {
+    if (value == null) {
+      remove(key);
+    } else {
+      this[key] = value;
+    }
+  }
+}
+
+extension on List<Uri> {
+  List<String> toJson() => [for (final uri in this) uri.toFilePath()];
+}
+
+extension<K extends Comparable<K>, V extends Object?> on Map<K, V> {
+  void sortOnKey() {
+    final result = <K, V>{};
+    final keysSorted = keys.toList()..sort();
+    for (final key in keysSorted) {
+      result[key] = this[key] as V;
+    }
+    clear();
+    addAll(result);
+  }
+}
diff --git a/pkgs/pub_formats/pubspec.yaml b/pkgs/pub_formats/pubspec.yaml
new file mode 100644
index 0000000..973cd26
--- /dev/null
+++ b/pkgs/pub_formats/pubspec.yaml
@@ -0,0 +1,26 @@
+name: pub_formats
+description: >-
+  An internal library containing file formats for the pub client. This library
+  is not meant to have a stable API and might break at any point when
+  `package:json_syntax_generator` is updated. This API is used in the Dart SDK,
+  so when doing breaking changes please roll ASAP to the Dart SDK.
+
+# This library is not meant to be published. It is used only in the Dart SDK.
+publish_to: none
+
+version: 0.0.1-wip
+
+resolution: workspace
+
+environment:
+  sdk: '>=3.9.0-21.0.dev <4.0.0'
+
+dev_dependencies:
+  args: ^2.6.0
+  json_schema: ^5.2.0 # May only be used in tool/ and test/json_schema/.
+  json_syntax_generator:
+    path: ../json_syntax_generator/
+  native_test_helpers:
+    path: ../native_test_helpers/
+  test: ^1.25.15
+  yaml: ^3.1.3
diff --git a/pkgs/pub_formats/test/pubspec_lock_test.dart b/pkgs/pub_formats/test/pubspec_lock_test.dart
new file mode 100644
index 0000000..3ddfa43
--- /dev/null
+++ b/pkgs/pub_formats/test/pubspec_lock_test.dart
@@ -0,0 +1,54 @@
+// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:native_test_helpers/native_test_helpers.dart';
+import 'package:pub_formats/pubspec_formats.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+void main() {
+  test('pubspec lock', () {
+    final packageRoot = findPackageRoot('pub_formats');
+    final pubspecFile = File.fromUri(
+      packageRoot.resolve('test_data/pubspec_lock_1.yaml'),
+    );
+    final json = _convertYamlMapToJsonMap(
+      loadYaml(pubspecFile.readAsStringSync()),
+    );
+    final parsed = PubspecLockFileSyntax.fromJson(json);
+    final package = parsed.packages!['dart_apitool']!;
+    expect(package.source, PackageSourceSyntax.hosted);
+
+    final description = HostedPackageDescriptionSyntax.fromJson(
+      package.description.json,
+    );
+    expect(description.sha256, isNotEmpty);
+  });
+}
+
+Map<String, Object?> _convertYamlMapToJsonMap(YamlMap yamlMap) {
+  final Map<String, Object?> jsonMap = {};
+  yamlMap.forEach((key, value) {
+    if (key is! String) {
+      // Handle non-string keys if your YAML allows them, or throw an error.
+      // For typical JSON conversion, keys are expected to be strings.
+      throw ArgumentError('YAML map keys must be strings for JSON conversion.');
+    }
+    jsonMap[key] = _convertYamlValue(value);
+  });
+  return jsonMap;
+}
+
+Object? _convertYamlValue(dynamic yamlValue) {
+  if (yamlValue is YamlMap) {
+    return _convertYamlMapToJsonMap(yamlValue);
+  } else if (yamlValue is YamlList) {
+    return yamlValue.map((e) => _convertYamlValue(e)).toList();
+  } else {
+    // For primitive types (String, int, double, bool, null)
+    return yamlValue;
+  }
+}
diff --git a/pkgs/pub_formats/test_data/pubspec_lock_1.yaml b/pkgs/pub_formats/test_data/pubspec_lock_1.yaml
new file mode 100644
index 0000000..f8d75c1
--- /dev/null
+++ b/pkgs/pub_formats/test_data/pubspec_lock_1.yaml
@@ -0,0 +1,349 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
+      url: 'https://pub.dev'
+    source: hosted
+    version: '85.0.0'
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      sha256: b1ade5707ab7a90dfd519eaac78a7184341d19adb6096c68d499b59c7c6cf880
+      url: 'https://pub.dev'
+    source: hosted
+    version: '7.7.0'
+  ansicolor:
+    dependency: transitive
+    description:
+      name: ansicolor
+      sha256: '50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.0.3'
+  archive:
+    dependency: transitive
+    description:
+      name: archive
+      sha256: '2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '4.0.7'
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.7.0'
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: '758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.13.0'
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.4.1'
+  checked_yaml:
+    dependency: transitive
+    description:
+      name: checked_yaml
+      sha256: '959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.0.4'
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: '2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.19.1'
+  colorize:
+    dependency: transitive
+    description:
+      name: colorize
+      sha256: '584746cd6ba1cba0633b6720f494fe6f9601c4170f0666c1579d2aa2a61071ba'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '3.0.0'
+  colorize_lumberdash:
+    dependency: transitive
+    description:
+      name: colorize_lumberdash
+      sha256: '6069ad908445caab046e93738c4f941536b6266076f2998b4ac6c012355eb1ba'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '3.0.0'
+  console:
+    dependency: transitive
+    description:
+      name: console
+      sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a
+      url: 'https://pub.dev'
+    source: hosted
+    version: '4.1.0'
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
+      url: 'https://pub.dev'
+    source: hosted
+    version: '3.1.2'
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: '1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '3.0.6'
+  dart_apitool:
+    dependency: 'direct main'
+    description:
+      name: dart_apitool
+      sha256: '8950232e78406e5734d0dee9e61ec3c20092279d985dca17be7382608df7b67b'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '0.21.1'
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      sha256: '289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.1.4'
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+      url: 'https://pub.dev'
+    source: hosted
+    version: '7.0.1'
+  freezed_annotation:
+    dependency: transitive
+    description:
+      name: freezed_annotation
+      sha256: '7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '3.1.0'
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.1.3'
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      sha256: '1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '4.9.0'
+  lumberdash:
+    dependency: transitive
+    description:
+      name: lumberdash
+      sha256: '1bc3750c094adb7f213a61883ca9878ba052fde52d9974b9039598c651fe096c'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '3.0.0'
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: '23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.17.0'
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.2.0'
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: '75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.9.1'
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      sha256: '9436fe11f82d7cc1642a8671e5aa4149ffa9ae9116e6cf6dd665fc0653e3825c'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '7.0.0'
+  plist_parser:
+    dependency: transitive
+    description:
+      name: plist_parser
+      sha256: e2a6f9abfa0c45c0253656b7360abb0dfb84af9937bace74605b93d2aad2bf0c
+      url: 'https://pub.dev'
+    source: hosted
+    version: '0.0.11'
+  posix:
+    dependency: transitive
+    description:
+      name: posix
+      sha256: '6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '6.0.3'
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      sha256: '5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.2.0'
+  pubspec_manager:
+    dependency: transitive
+    description:
+      name: pubspec_manager
+      sha256: '416609635f6c53ab759223ae66e71282e61a863c13ec47d60381e4b8046c2a71'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.0.3'
+  pubspec_parse:
+    dependency: transitive
+    description:
+      name: pubspec_parse
+      sha256: '0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.5.0'
+  simple_sparse_list:
+    dependency: transitive
+    description:
+      name: simple_sparse_list
+      sha256: aa648fd240fa39b49dcd11c19c266990006006de6699a412de485695910fbc1f
+      url: 'https://pub.dev'
+    source: hosted
+    version: '0.1.4'
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: '254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.10.1'
+  stack:
+    dependency: transitive
+    description:
+      name: stack
+      sha256: f5a3032c965b74f394dc6aa138c82373a3403438c68cf5d8e6ef2bc2698949bf
+      url: 'https://pub.dev'
+    source: hosted
+    version: '0.2.2'
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: '921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.4.1'
+  strings:
+    dependency: transitive
+    description:
+      name: strings
+      sha256: ff7373d0221207fe3b834953775c0e6a492b37f4409a3d539d35a34a7f50c38b
+      url: 'https://pub.dev'
+    source: hosted
+    version: '4.0.0'
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: '7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.2.2'
+  tuple:
+    dependency: transitive
+    description:
+      name: tuple
+      sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.0.2'
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.4.0'
+  unicode:
+    dependency: transitive
+    description:
+      name: unicode
+      sha256: '0d99edbd2e74726bed2e4989713c8bec02e5581628e334d8c88c0271593fb402'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.1.8'
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+      url: 'https://pub.dev'
+    source: hosted
+    version: '2.2.0'
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      sha256: '0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '1.1.2'
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      sha256: '3202a47961c1a0af6097c9f8c1b492d705248ba309e6f7a72410422c05046851'
+      url: 'https://pub.dev'
+    source: hosted
+    version: '6.6.0'
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+      url: 'https://pub.dev'
+    source: hosted
+    version: '3.1.3'
+sdks:
+  dart: '>=3.9.0-293.0.dev <4.0.0'
diff --git a/pkgs/pub_formats/tool/generate.dart b/pkgs/pub_formats/tool/generate.dart
new file mode 100644
index 0000000..3f0c186
--- /dev/null
+++ b/pkgs/pub_formats/tool/generate.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:json_schema/json_schema.dart';
+import 'package:json_syntax_generator/json_syntax_generator.dart';
+import 'package:native_test_helpers/native_test_helpers.dart';
+
+void main(List<String> args) {
+  final packageRoot = findPackageRoot('pub_formats');
+
+  final stopwatch = Stopwatch()..start();
+  final parser = ArgParser()
+    ..addFlag(
+      'set-exit-if-changed',
+      negatable: false,
+      help: 'Return a non-zero exit code if any files were changed.',
+    )
+    ..addFlag(
+      'dump',
+      abbr: 'd',
+      negatable: false,
+      help: 'Dump analyzed schema to .g.txt file.',
+    );
+  final argResults = parser.parse(args);
+
+  final setExitIfChanged = argResults['set-exit-if-changed'] as bool;
+  final dumpAnalyzedSchema = argResults['dump'] as bool;
+  var generatedCount = 0;
+  var changedCount = 0;
+
+  final schemaFile = File.fromUri(
+    packageRoot.resolve('doc/schema/pubspec_lock.schema.json'),
+  );
+  final schemaJson = jsonDecode(schemaFile.readAsStringSync());
+  final schema = JsonSchema.create(schemaJson);
+
+  final analyzedSchema = SchemaAnalyzer(
+    schema,
+    nameOverrides: {'path': 'path\$'},
+  ).analyze();
+  final textDumpFile = File.fromUri(
+    packageRoot.resolve('lib/src/pubspec_lock_syntax.g.txt'),
+  );
+  final newTextDumpContent = analyzedSchema.toString();
+  if (dumpAnalyzedSchema) {
+    var oldTextDumpContent = '';
+    if (textDumpFile.existsSync()) {
+      oldTextDumpContent = textDumpFile.readAsStringSync();
+    }
+    if (oldTextDumpContent != newTextDumpContent) {
+      textDumpFile.writeAsStringSync(newTextDumpContent);
+      print('Generated ${textDumpFile.uri}');
+      changedCount += 1;
+    }
+    generatedCount += 1;
+  } else if (textDumpFile.existsSync()) {
+    textDumpFile.deleteSync();
+    print('Deleted ${textDumpFile.uri}');
+    changedCount += 1;
+  }
+
+  final output = SyntaxGenerator(
+    analyzedSchema,
+    header: '''
+// This file is generated, do not edit.
+// File generated by pkgs/pub_formats/tool/generate.dart. 
+// Must be rerun when pkgs/pub_formats/doc/schema/ is modified.
+''',
+  ).generate();
+  final outputUri = packageRoot.resolve('lib/src/pubspec_lock_syntax.g.dart');
+  final outputFile = File.fromUri(outputUri);
+  var oldOutputContent = '';
+  if (outputFile.existsSync()) {
+    oldOutputContent = outputFile.readAsStringSync();
+  }
+
+  if (oldOutputContent != output) {
+    outputFile.writeAsStringSync(output);
+  }
+
+  Process.runSync(Platform.executable, ['format', outputUri.toFilePath()]);
+
+  final newOutputContent = outputFile.readAsStringSync();
+
+  final newContentNormalized = newOutputContent.replaceAll('\r\n', '\n');
+  final oldContentNormalized = oldOutputContent.replaceAll('\r\n', '\n');
+  if (newContentNormalized != oldContentNormalized) {
+    print('Generated $outputUri');
+    changedCount += 1;
+  }
+  generatedCount += 1;
+
+  stopwatch.stop();
+  final duration = stopwatch.elapsedMilliseconds / 1000.0;
+  print(
+    'Generated $generatedCount files ($changedCount changed) in '
+    '${duration.toStringAsFixed(2)} seconds.',
+  );
+  if (setExitIfChanged && changedCount > 0) {
+    exit(1);
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 3a68ee6..876bb7b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -58,6 +58,7 @@
   - pkgs/hooks/example/link/package_with_assets
   - pkgs/json_syntax_generator
   - pkgs/native_test_helpers
+  - pkgs/pub_formats
   - pkgs/native_toolchain_c
   - pkgs/repo_lint_rules
 
diff --git a/tool/ci.dart b/tool/ci.dart
index 16dd954..d1d0267 100644
--- a/tool/ci.dart
+++ b/tool/ci.dart
@@ -58,6 +58,7 @@
       'pkgs/hooks/tool/generate_schemas.dart',
       'pkgs/hooks/tool/generate_syntax.dart',
       'pkgs/hooks/tool/normalize.dart',
+      'pkgs/pub_formats/tool/generate.dart',
     ];
     for (final generator in generators) {
       _runProcess('dart', [generator, '--set-exit-if-changed']);