diff --git a/pkgs/code_builder/CHANGELOG.md b/pkgs/code_builder/CHANGELOG.md
index 471f72f..1d86e5c 100644
--- a/pkgs/code_builder/CHANGELOG.md
+++ b/pkgs/code_builder/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 * Upgrade `dart_style` and `source_gen` to remove `package:macros` dependency.
 * Require Dart `^3.6.0` due to the upgrades.
+* Support `Expression.newInstanceNamed` with empty name
 
 ## 4.10.1
 
diff --git a/pkgs/code_builder/lib/src/specs/expression.dart b/pkgs/code_builder/lib/src/specs/expression.dart
index b9193e6..aa06de2 100644
--- a/pkgs/code_builder/lib/src/specs/expression.dart
+++ b/pkgs/code_builder/lib/src/specs/expression.dart
@@ -578,10 +578,10 @@
     final out = output ??= StringBuffer();
     return _writeConstExpression(out, expression.isConst, () {
       expression.target.accept(this, out);
-      if (expression.name != null) {
+      if (expression.name case final name? when name.isNotEmpty) {
         out
           ..write('.')
-          ..write(expression.name);
+          ..write(name);
       }
       if (expression.typeArguments.isNotEmpty) {
         out.write('<');
diff --git a/pkgs/code_builder/test/specs/code/expression_test.dart b/pkgs/code_builder/test/specs/code/expression_test.dart
index 4ce9eba..7a424fd 100644
--- a/pkgs/code_builder/test/specs/code/expression_test.dart
+++ b/pkgs/code_builder/test/specs/code/expression_test.dart
@@ -251,6 +251,13 @@
     );
   });
 
+  test('should emit invoking unnamed constructor when name is empty', () {
+    expect(
+      refer('Foo').newInstanceNamed('', []),
+      equalsDart('Foo()'),
+    );
+  });
+
   test('should emit invoking const Type()', () {
     expect(
       refer('Object').constInstance([]),
diff --git a/pkgs/json_rpc_2/CHANGELOG.md b/pkgs/json_rpc_2/CHANGELOG.md
index 1f2cf8e..0ab99da 100644
--- a/pkgs/json_rpc_2/CHANGELOG.md
+++ b/pkgs/json_rpc_2/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 4.0.0
+
+* Add custom ID generator option to clients, which allows for `String` ids.
+* **Breaking**: When `String` ids are present in a response, we no longer
+  automatically try to parse them as integers. This behavior was never a part
+  of the spec, and is not compatible with allowing custom ID generators.
+
 ## 3.0.3
 
 * Require Dart 3.4
diff --git a/pkgs/json_rpc_2/lib/src/client.dart b/pkgs/json_rpc_2/lib/src/client.dart
index 182f945..388c601 100644
--- a/pkgs/json_rpc_2/lib/src/client.dart
+++ b/pkgs/json_rpc_2/lib/src/client.dart
@@ -18,8 +18,8 @@
 class Client {
   final StreamChannel<dynamic> _channel;
 
-  /// The next request id.
-  var _id = 0;
+  /// A function to generate the next request id.
+  Object Function() _idGenerator;
 
   /// The current batch of requests to be sent together.
   ///
@@ -27,7 +27,9 @@
   List<Map<String, dynamic>>? _batch;
 
   /// The map of request ids to pending requests.
-  final _pendingRequests = <int, _Request>{};
+  ///
+  /// Keys must be of type `int` or `String`.
+  final _pendingRequests = <Object, _Request>{};
 
   final _done = Completer<void>();
 
@@ -49,9 +51,14 @@
   ///
   /// Note that the client won't begin listening to [channel] until
   /// [Client.listen] is called.
-  Client(StreamChannel<String> channel)
+  ///
+  /// If [idGenerator] is passed, it will be called to generate an ID for each
+  /// request. Defaults to an auto-incrementing `int`. The value returned must
+  /// be either an `int` or `String`.
+  Client(StreamChannel<String> channel, {Object Function()? idGenerator})
       : this.withoutJson(
-            jsonDocument.bind(channel).transformStream(ignoreFormatExceptions));
+            jsonDocument.bind(channel).transformStream(ignoreFormatExceptions),
+            idGenerator: idGenerator);
 
   /// Creates a [Client] that communicates using decoded messages over
   /// [_channel].
@@ -61,7 +68,12 @@
   ///
   /// Note that the client won't begin listening to [_channel] until
   /// [Client.listen] is called.
-  Client.withoutJson(this._channel) {
+  ///
+  /// If [_idGenerator] is passed, it will be called to generate an ID for each
+  /// request. Defaults to an auto-incrementing `int`. The value returned must
+  /// be either an `int` or `String`.
+  Client.withoutJson(this._channel, {Object Function()? idGenerator})
+      : _idGenerator = idGenerator ?? _createIncrementingIdGenerator() {
     done.whenComplete(() {
       for (var request in _pendingRequests.values) {
         request.completer.completeError(StateError(
@@ -115,7 +127,7 @@
   /// Throws a [StateError] if the client is closed while the request is in
   /// flight, or if the client is closed when this method is called.
   Future<Object?> sendRequest(String method, [Object? parameters]) {
-    var id = _id++;
+    var id = _idGenerator();
     _send(method, parameters, id);
 
     var completer = Completer<Object?>.sync();
@@ -142,7 +154,7 @@
   ///
   /// Sends a request to invoke [method] with [parameters]. If [id] is given,
   /// the request uses that id.
-  void _send(String method, Object? parameters, [int? id]) {
+  void _send(String method, Object? parameters, [Object? id]) {
     if (parameters is Iterable) parameters = parameters.toList();
     if (parameters is! Map && parameters is! List && parameters != null) {
       throw ArgumentError('Only maps and lists may be used as JSON-RPC '
@@ -201,7 +213,6 @@
     if (!_isResponseValid(response_)) return;
     final response = response_ as Map;
     var id = response['id'];
-    id = (id is String) ? int.parse(id) : id;
     var request = _pendingRequests.remove(id)!;
     if (response.containsKey('result')) {
       request.completer.complete(response['result']);
@@ -218,7 +229,6 @@
     if (response is! Map) return false;
     if (response['jsonrpc'] != '2.0') return false;
     var id = response['id'];
-    id = (id is String) ? int.parse(id) : id;
     if (!_pendingRequests.containsKey(id)) return false;
     if (response.containsKey('result')) return true;
 
@@ -244,3 +254,11 @@
 
   _Request(this.method, this.completer, this.chain);
 }
+
+/// The default ID generator, uses an auto incrementing integer.
+///
+/// Each call returns a new function which starts back a `0`.
+int Function() _createIncrementingIdGenerator() {
+  var nextId = 0;
+  return () => nextId++;
+}
diff --git a/pkgs/json_rpc_2/lib/src/peer.dart b/pkgs/json_rpc_2/lib/src/peer.dart
index 677b6e1..71d9093 100644
--- a/pkgs/json_rpc_2/lib/src/peer.dart
+++ b/pkgs/json_rpc_2/lib/src/peer.dart
@@ -59,12 +59,20 @@
   /// some requests which are not conformant with the JSON-RPC 2.0
   /// specification. In particular, requests missing the `jsonrpc` parameter
   /// will be accepted.
-  Peer(StreamChannel<String> channel,
-      {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true})
-      : this.withoutJson(
+  ///
+  /// If [idGenerator] is passed, it will be called to generate an ID for each
+  /// request. Defaults to an auto-incrementing `int`.  The value returned must
+  /// be either an `int` or `String`.
+  Peer(
+    StreamChannel<String> channel, {
+    ErrorCallback? onUnhandledError,
+    bool strictProtocolChecks = true,
+    Object Function()? idGenerator,
+  }) : this.withoutJson(
             jsonDocument.bind(channel).transform(respondToFormatExceptions),
             onUnhandledError: onUnhandledError,
-            strictProtocolChecks: strictProtocolChecks);
+            strictProtocolChecks: strictProtocolChecks,
+            idGenerator: idGenerator);
 
   /// Creates a [Peer] that communicates using decoded messages over [_channel].
   ///
@@ -81,14 +89,24 @@
   /// some requests which are not conformant with the JSON-RPC 2.0
   /// specification. In particular, requests missing the `jsonrpc` parameter
   /// will be accepted.
+  ///
+  /// If [idGenerator] is passed, it will be called to generate an ID for each
+  /// request. Defaults to an auto-incrementing `int`. The value returned must
+  /// be either an `int` or `String`.
   Peer.withoutJson(this._channel,
-      {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true}) {
+      {ErrorCallback? onUnhandledError,
+      bool strictProtocolChecks = true,
+      Object Function()? idGenerator}) {
     _server = Server.withoutJson(
         StreamChannel(_serverIncomingForwarder.stream, _channel.sink),
         onUnhandledError: onUnhandledError,
         strictProtocolChecks: strictProtocolChecks);
     _client = Client.withoutJson(
-        StreamChannel(_clientIncomingForwarder.stream, _channel.sink));
+        StreamChannel(
+          _clientIncomingForwarder.stream,
+          _channel.sink,
+        ),
+        idGenerator: idGenerator);
   }
 
   // Client methods.
diff --git a/pkgs/json_rpc_2/pubspec.yaml b/pkgs/json_rpc_2/pubspec.yaml
index 7b42278..ad4e839 100644
--- a/pkgs/json_rpc_2/pubspec.yaml
+++ b/pkgs/json_rpc_2/pubspec.yaml
@@ -1,5 +1,5 @@
 name: json_rpc_2
-version: 3.0.3
+version: 4.0.0
 description: >-
   Utilities to write a client or server using the JSON-RPC 2.0 spec.
 repository: https://github.com/dart-lang/tools/tree/main/pkgs/json_rpc_2
diff --git a/pkgs/json_rpc_2/test/client/client_test.dart b/pkgs/json_rpc_2/test/client/client_test.dart
index 1a4f65d..d907305 100644
--- a/pkgs/json_rpc_2/test/client/client_test.dart
+++ b/pkgs/json_rpc_2/test/client/client_test.dart
@@ -11,16 +11,212 @@
 void main() {
   late ClientController controller;
 
-  setUp(() => controller = ClientController());
+  group('Default options', () {
+    setUp(() => controller = ClientController());
 
-  test('sends a message and returns the response', () {
+    test('sends a message and returns the response', () {
+      controller.expectRequest((request) {
+        expect(
+            request,
+            allOf([
+              containsPair('jsonrpc', '2.0'),
+              containsPair('method', 'foo'),
+              containsPair('params', {'param': 'value'})
+            ]));
+
+        return {'jsonrpc': '2.0', 'result': 'bar', 'id': request['id']};
+      });
+
+      expect(controller.client.sendRequest('foo', {'param': 'value'}),
+          completion(equals('bar')));
+    });
+
+    test('sends a notification and expects no response', () {
+      controller.expectRequest((request) {
+        expect(
+            request,
+            equals({
+              'jsonrpc': '2.0',
+              'method': 'foo',
+              'params': {'param': 'value'}
+            }));
+      });
+
+      controller.client.sendNotification('foo', {'param': 'value'});
+    });
+
+    test('sends a notification with positional parameters', () {
+      controller.expectRequest((request) {
+        expect(
+            request,
+            equals({
+              'jsonrpc': '2.0',
+              'method': 'foo',
+              'params': ['value1', 'value2']
+            }));
+      });
+
+      controller.client.sendNotification('foo', ['value1', 'value2']);
+    });
+
+    test('sends a notification with no parameters', () {
+      controller.expectRequest((request) {
+        expect(request, equals({'jsonrpc': '2.0', 'method': 'foo'}));
+      });
+
+      controller.client.sendNotification('foo');
+    });
+
+    test('sends a synchronous batch of requests', () {
+      controller.expectRequest((request) {
+        expect(request, isA<List>());
+        expect(request, hasLength(3));
+        expect(request[0], equals({'jsonrpc': '2.0', 'method': 'foo'}));
+        expect(
+            request[1],
+            allOf([
+              containsPair('jsonrpc', '2.0'),
+              containsPair('method', 'bar'),
+              containsPair('params', {'param': 'value'})
+            ]));
+        expect(
+            request[2],
+            allOf([
+              containsPair('jsonrpc', '2.0'),
+              containsPair('method', 'baz')
+            ]));
+
+        return [
+          {'jsonrpc': '2.0', 'result': 'baz response', 'id': request[2]['id']},
+          {'jsonrpc': '2.0', 'result': 'bar response', 'id': request[1]['id']}
+        ];
+      });
+
+      controller.client.withBatch(() {
+        controller.client.sendNotification('foo');
+        expect(controller.client.sendRequest('bar', {'param': 'value'}),
+            completion(equals('bar response')));
+        expect(controller.client.sendRequest('baz'),
+            completion(equals('baz response')));
+      });
+    });
+
+    test('sends an asynchronous batch of requests', () {
+      controller.expectRequest((request) {
+        expect(request, isA<List>());
+        expect(request, hasLength(3));
+        expect(request[0], equals({'jsonrpc': '2.0', 'method': 'foo'}));
+        expect(
+            request[1],
+            allOf([
+              containsPair('jsonrpc', '2.0'),
+              containsPair('method', 'bar'),
+              containsPair('params', {'param': 'value'})
+            ]));
+        expect(
+            request[2],
+            allOf([
+              containsPair('jsonrpc', '2.0'),
+              containsPair('method', 'baz')
+            ]));
+
+        return [
+          {'jsonrpc': '2.0', 'result': 'baz response', 'id': request[2]['id']},
+          {'jsonrpc': '2.0', 'result': 'bar response', 'id': request[1]['id']}
+        ];
+      });
+
+      controller.client.withBatch(() {
+        return Future<void>.value().then<void>((_) {
+          controller.client.sendNotification('foo');
+        }).then<void>((_) {
+          expect(controller.client.sendRequest('bar', {'param': 'value'}),
+              completion(equals('bar response')));
+        }).then<void>((_) {
+          expect(controller.client.sendRequest('baz'),
+              completion(equals('baz response')));
+        });
+      });
+    });
+
+    test('reports an error from the server', () {
+      controller.expectRequest((request) {
+        expect(
+            request,
+            allOf([
+              containsPair('jsonrpc', '2.0'),
+              containsPair('method', 'foo')
+            ]));
+
+        return {
+          'jsonrpc': '2.0',
+          'error': {
+            'code': error_code.SERVER_ERROR,
+            'message': 'you are bad at requests',
+            'data': 'some junk'
+          },
+          'id': request['id']
+        };
+      });
+
+      expect(
+          controller.client.sendRequest('foo', {'param': 'value'}),
+          throwsA(isA<json_rpc.RpcException>()
+              .having((e) => e.code, 'code', error_code.SERVER_ERROR)
+              .having((e) => e.message, 'message', 'you are bad at requests')
+              .having((e) => e.data, 'data', 'some junk')));
+    });
+
+    test('requests throw StateErrors if the client is closed', () {
+      controller.client.close();
+      expect(() => controller.client.sendRequest('foo'), throwsStateError);
+      expect(() => controller.client.sendNotification('foo'), throwsStateError);
+    });
+
+    test('ignores bogus responses', () {
+      // Make a request so we have something to respond to.
+      controller.expectRequest((request) {
+        controller.sendJsonResponse('{invalid');
+        controller.sendResponse('not a map');
+        controller.sendResponse({
+          'jsonrpc': 'wrong version',
+          'result': 'wrong',
+          'id': request['id']
+        });
+        controller.sendResponse({'jsonrpc': '2.0', 'result': 'wrong'});
+        controller.sendResponse({'jsonrpc': '2.0', 'id': request['id']});
+        controller.sendResponse(
+            {'jsonrpc': '2.0', 'error': 'not a map', 'id': request['id']});
+        controller.sendResponse({
+          'jsonrpc': '2.0',
+          'error': {'code': 'not an int', 'message': 'dang yo'},
+          'id': request['id']
+        });
+        controller.sendResponse({
+          'jsonrpc': '2.0',
+          'error': {'code': 123, 'message': 0xDEADBEEF},
+          'id': request['id']
+        });
+
+        return pumpEventQueue().then(
+            (_) => {'jsonrpc': '2.0', 'result': 'right', 'id': request['id']});
+      });
+
+      expect(controller.client.sendRequest('foo'), completion(equals('right')));
+    });
+  });
+
+  test('with custom String ids', () {
+    var id = 0;
+    controller = ClientController(idGenerator: () => 'ID-${id++}');
     controller.expectRequest((request) {
       expect(
           request,
           allOf([
             containsPair('jsonrpc', '2.0'),
             containsPair('method', 'foo'),
-            containsPair('params', {'param': 'value'})
+            containsPair('params', {'param': 'value'}),
+            containsPair('id', 'ID-0'),
           ]));
 
       return {'jsonrpc': '2.0', 'result': 'bar', 'id': request['id']};
@@ -30,189 +226,23 @@
         completion(equals('bar')));
   });
 
-  test('sends a message and returns the response with String id', () {
+  test('String ids are not parsed as ints', () {
+    var id = 0;
+    controller = ClientController(idGenerator: () => '${id++}');
     controller.expectRequest((request) {
       expect(
           request,
           allOf([
             containsPair('jsonrpc', '2.0'),
             containsPair('method', 'foo'),
-            containsPair('params', {'param': 'value'})
+            containsPair('params', {'param': 'value'}),
+            containsPair('id', '0'),
           ]));
 
-      return {
-        'jsonrpc': '2.0',
-        'result': 'bar',
-        'id': request['id'].toString()
-      };
+      return {'jsonrpc': '2.0', 'result': 'bar', 'id': request['id']};
     });
 
     expect(controller.client.sendRequest('foo', {'param': 'value'}),
         completion(equals('bar')));
   });
-
-  test('sends a notification and expects no response', () {
-    controller.expectRequest((request) {
-      expect(
-          request,
-          equals({
-            'jsonrpc': '2.0',
-            'method': 'foo',
-            'params': {'param': 'value'}
-          }));
-    });
-
-    controller.client.sendNotification('foo', {'param': 'value'});
-  });
-
-  test('sends a notification with positional parameters', () {
-    controller.expectRequest((request) {
-      expect(
-          request,
-          equals({
-            'jsonrpc': '2.0',
-            'method': 'foo',
-            'params': ['value1', 'value2']
-          }));
-    });
-
-    controller.client.sendNotification('foo', ['value1', 'value2']);
-  });
-
-  test('sends a notification with no parameters', () {
-    controller.expectRequest((request) {
-      expect(request, equals({'jsonrpc': '2.0', 'method': 'foo'}));
-    });
-
-    controller.client.sendNotification('foo');
-  });
-
-  test('sends a synchronous batch of requests', () {
-    controller.expectRequest((request) {
-      expect(request, isA<List>());
-      expect(request, hasLength(3));
-      expect(request[0], equals({'jsonrpc': '2.0', 'method': 'foo'}));
-      expect(
-          request[1],
-          allOf([
-            containsPair('jsonrpc', '2.0'),
-            containsPair('method', 'bar'),
-            containsPair('params', {'param': 'value'})
-          ]));
-      expect(
-          request[2],
-          allOf(
-              [containsPair('jsonrpc', '2.0'), containsPair('method', 'baz')]));
-
-      return [
-        {'jsonrpc': '2.0', 'result': 'baz response', 'id': request[2]['id']},
-        {'jsonrpc': '2.0', 'result': 'bar response', 'id': request[1]['id']}
-      ];
-    });
-
-    controller.client.withBatch(() {
-      controller.client.sendNotification('foo');
-      expect(controller.client.sendRequest('bar', {'param': 'value'}),
-          completion(equals('bar response')));
-      expect(controller.client.sendRequest('baz'),
-          completion(equals('baz response')));
-    });
-  });
-
-  test('sends an asynchronous batch of requests', () {
-    controller.expectRequest((request) {
-      expect(request, isA<List>());
-      expect(request, hasLength(3));
-      expect(request[0], equals({'jsonrpc': '2.0', 'method': 'foo'}));
-      expect(
-          request[1],
-          allOf([
-            containsPair('jsonrpc', '2.0'),
-            containsPair('method', 'bar'),
-            containsPair('params', {'param': 'value'})
-          ]));
-      expect(
-          request[2],
-          allOf(
-              [containsPair('jsonrpc', '2.0'), containsPair('method', 'baz')]));
-
-      return [
-        {'jsonrpc': '2.0', 'result': 'baz response', 'id': request[2]['id']},
-        {'jsonrpc': '2.0', 'result': 'bar response', 'id': request[1]['id']}
-      ];
-    });
-
-    controller.client.withBatch(() {
-      return Future<void>.value().then<void>((_) {
-        controller.client.sendNotification('foo');
-      }).then<void>((_) {
-        expect(controller.client.sendRequest('bar', {'param': 'value'}),
-            completion(equals('bar response')));
-      }).then<void>((_) {
-        expect(controller.client.sendRequest('baz'),
-            completion(equals('baz response')));
-      });
-    });
-  });
-
-  test('reports an error from the server', () {
-    controller.expectRequest((request) {
-      expect(
-          request,
-          allOf(
-              [containsPair('jsonrpc', '2.0'), containsPair('method', 'foo')]));
-
-      return {
-        'jsonrpc': '2.0',
-        'error': {
-          'code': error_code.SERVER_ERROR,
-          'message': 'you are bad at requests',
-          'data': 'some junk'
-        },
-        'id': request['id']
-      };
-    });
-
-    expect(
-        controller.client.sendRequest('foo', {'param': 'value'}),
-        throwsA(isA<json_rpc.RpcException>()
-            .having((e) => e.code, 'code', error_code.SERVER_ERROR)
-            .having((e) => e.message, 'message', 'you are bad at requests')
-            .having((e) => e.data, 'data', 'some junk')));
-  });
-
-  test('requests throw StateErrors if the client is closed', () {
-    controller.client.close();
-    expect(() => controller.client.sendRequest('foo'), throwsStateError);
-    expect(() => controller.client.sendNotification('foo'), throwsStateError);
-  });
-
-  test('ignores bogus responses', () {
-    // Make a request so we have something to respond to.
-    controller.expectRequest((request) {
-      controller.sendJsonResponse('{invalid');
-      controller.sendResponse('not a map');
-      controller.sendResponse(
-          {'jsonrpc': 'wrong version', 'result': 'wrong', 'id': request['id']});
-      controller.sendResponse({'jsonrpc': '2.0', 'result': 'wrong'});
-      controller.sendResponse({'jsonrpc': '2.0', 'id': request['id']});
-      controller.sendResponse(
-          {'jsonrpc': '2.0', 'error': 'not a map', 'id': request['id']});
-      controller.sendResponse({
-        'jsonrpc': '2.0',
-        'error': {'code': 'not an int', 'message': 'dang yo'},
-        'id': request['id']
-      });
-      controller.sendResponse({
-        'jsonrpc': '2.0',
-        'error': {'code': 123, 'message': 0xDEADBEEF},
-        'id': request['id']
-      });
-
-      return pumpEventQueue().then(
-          (_) => {'jsonrpc': '2.0', 'result': 'right', 'id': request['id']});
-    });
-
-    expect(controller.client.sendRequest('foo'), completion(equals('right')));
-  });
 }
diff --git a/pkgs/json_rpc_2/test/client/utils.dart b/pkgs/json_rpc_2/test/client/utils.dart
index 38e187f..ed308e5 100644
--- a/pkgs/json_rpc_2/test/client/utils.dart
+++ b/pkgs/json_rpc_2/test/client/utils.dart
@@ -20,9 +20,10 @@
   /// The client.
   late final json_rpc.Client client;
 
-  ClientController() {
+  ClientController({Object Function()? idGenerator}) {
     client = json_rpc.Client(
-        StreamChannel(_responseController.stream, _requestController.sink));
+        StreamChannel(_responseController.stream, _requestController.sink),
+        idGenerator: idGenerator);
     client.listen();
   }