[pigeon] Fix java crash for nullable nested type (#1192)

diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 2e8c814..d3afa4f 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,4 +1,8 @@
-## 2.0.1
+## 2.0.2
+
+* Fixes Java crash for nullable nested type.
+  
+* ## 2.0.1
 
 * Adds support for TaskQueues for serial background execution.
 
diff --git a/packages/pigeon/bin/run_tests.dart b/packages/pigeon/bin/run_tests.dart
index a506b78..a65d02e 100644
--- a/packages/pigeon/bin/run_tests.dart
+++ b/packages/pigeon/bin/run_tests.dart
@@ -144,6 +144,8 @@
         '$flutterUnitTestsPath/lib/multiple_arity.gen.dart',
     'pigeons/non_null_fields.dart':
         '$flutterUnitTestsPath/lib/non_null_fields.gen.dart',
+    'pigeons/null_fields.dart':
+        '$flutterUnitTestsPath/lib/null_fields.gen.dart',
     'pigeons/nullable_returns.dart':
         '$flutterUnitTestsPath/lib/nullable_returns.gen.dart',
   });
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 107f035..15272f7 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -8,7 +8,7 @@
 import 'ast.dart';
 
 /// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '2.0.1';
+const String pigeonVersion = '2.0.2';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
index efa210c..8d0b15a 100644
--- a/packages/pigeon/lib/java_generator.dart
+++ b/packages/pigeon/lib/java_generator.dart
@@ -435,7 +435,7 @@
     return '($varName == null) ? null : (($varName instanceof Integer) ? (Integer)$varName : (${hostDatatype.datatype})$varName)';
   } else if (!hostDatatype.isBuiltin &&
       classes.map((Class x) => x.name).contains(field.type.baseName)) {
-    return '${hostDatatype.datatype}.fromMap((Map)$varName)';
+    return '($varName == null) ? null : ${hostDatatype.datatype}.fromMap((Map)$varName)';
   } else {
     return '(${hostDatatype.datatype})$varName';
   }
diff --git a/packages/pigeon/pigeons/null_fields.dart b/packages/pigeon/pigeons/null_fields.dart
new file mode 100644
index 0000000..574b4f6
--- /dev/null
+++ b/packages/pigeon/pigeons/null_fields.dart
@@ -0,0 +1,43 @@
+// Copyright 2013 The Flutter Authors. 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 an example pigeon file that is used in compilation, unit, mock
+// handler, and e2e tests.
+
+import 'package:pigeon/pigeon.dart';
+
+class NullFieldsSearchRequest {
+  NullFieldsSearchRequest(this.query);
+  String? query;
+}
+
+enum NullFieldsSearchReplyType {
+  success,
+  failure,
+}
+
+class NullFieldsSearchReply {
+  NullFieldsSearchReply(
+    this.result,
+    this.error,
+    this.indices,
+    this.request,
+    this.type,
+  );
+  String? result;
+  String? error;
+  List<int?>? indices;
+  NullFieldsSearchRequest? request;
+  NullFieldsSearchReplyType? type;
+}
+
+@HostApi()
+abstract class NullFieldsHostApi {
+  NullFieldsSearchReply search(NullFieldsSearchRequest nested);
+}
+
+@FlutterApi()
+abstract class NullFieldsFlutterApi {
+  NullFieldsSearchReply search(NullFieldsSearchRequest request);
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/NullFieldsTest.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/NullFieldsTest.java
new file mode 100644
index 0000000..1b88470
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/NullFieldsTest.java
@@ -0,0 +1,171 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_unit_tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+
+public class NullFieldsTest {
+  @Test
+  public void builderWithValues() {
+    NullFields.NullFieldsSearchRequest request =
+        new NullFields.NullFieldsSearchRequest.Builder().setQuery("hello").build();
+
+    NullFields.NullFieldsSearchReply reply =
+        new NullFields.NullFieldsSearchReply.Builder()
+            .setResult("result")
+            .setError("error")
+            .setIndices(Arrays.asList(1L, 2L, 3L))
+            .setRequest(request)
+            .setType(NullFields.NullFieldsSearchReplyType.success)
+            .build();
+
+    assertEquals(reply.getResult(), "result");
+    assertEquals(reply.getError(), "error");
+    assertEquals(reply.getIndices(), Arrays.asList(1L, 2L, 3L));
+    assertEquals(reply.getRequest().getQuery(), "hello");
+    assertEquals(reply.getType(), NullFields.NullFieldsSearchReplyType.success);
+  }
+
+  @Test
+  public void builderRequestWithNulls() {
+    NullFields.NullFieldsSearchRequest request =
+        new NullFields.NullFieldsSearchRequest.Builder().setQuery(null).build();
+  }
+
+  @Test
+  public void builderReplyWithNulls() {
+    NullFields.NullFieldsSearchReply reply =
+        new NullFields.NullFieldsSearchReply.Builder()
+            .setResult(null)
+            .setError(null)
+            .setIndices(null)
+            .setRequest(null)
+            .setType(null)
+            .build();
+
+    assertNull(reply.getResult());
+    assertNull(reply.getError());
+    assertNull(reply.getIndices());
+    assertNull(reply.getRequest());
+    assertNull(reply.getType());
+  }
+
+  @Test
+  public void requestFromMapWithValues() {
+    HashMap<String, Object> map = new HashMap<>();
+    map.put("query", "hello");
+
+    NullFields.NullFieldsSearchRequest request = NullFields.NullFieldsSearchRequest.fromMap(map);
+    assertEquals(request.getQuery(), "hello");
+  }
+
+  @Test
+  public void requestFromMapWithNulls() {
+    HashMap<String, Object> map = new HashMap<>();
+    map.put("query", null);
+
+    NullFields.NullFieldsSearchRequest request = NullFields.NullFieldsSearchRequest.fromMap(map);
+    assertNull(request.getQuery());
+  }
+
+  @Test
+  public void replyFromMapWithValues() {
+    HashMap<String, Object> requestMap = new HashMap<>();
+    requestMap.put("query", "hello");
+
+    HashMap<String, Object> map = new HashMap<>();
+    map.put("result", "result");
+    map.put("error", "error");
+    map.put("indices", Arrays.asList(1L, 2L, 3L));
+    map.put("request", requestMap);
+    map.put("type", NullFields.NullFieldsSearchReplyType.success.ordinal());
+
+    NullFields.NullFieldsSearchReply reply = NullFields.NullFieldsSearchReply.fromMap(map);
+    assertEquals(reply.getResult(), "result");
+    assertEquals(reply.getError(), "error");
+    assertEquals(reply.getIndices(), Arrays.asList(1L, 2L, 3L));
+    assertEquals(reply.getRequest().getQuery(), "hello");
+    assertEquals(reply.getType(), NullFields.NullFieldsSearchReplyType.success);
+  }
+
+  @Test
+  public void replyFromMapWithNulls() {
+    HashMap<String, Object> map = new HashMap<>();
+    map.put("result", null);
+    map.put("error", null);
+    map.put("indices", null);
+    map.put("request", null);
+    map.put("type", null);
+
+    NullFields.NullFieldsSearchReply reply = NullFields.NullFieldsSearchReply.fromMap(map);
+    assertNull(reply.getResult());
+    assertNull(reply.getError());
+    assertNull(reply.getIndices());
+    assertNull(reply.getRequest());
+    assertNull(reply.getType());
+  }
+
+  @Test
+  public void requestToMapWithValues() {
+    NullFields.NullFieldsSearchRequest request =
+        new NullFields.NullFieldsSearchRequest.Builder().setQuery("hello").build();
+
+    Map<String, Object> map = request.toMap();
+    assertEquals(map.get("query"), "hello");
+  }
+
+  @Test
+  public void requestToMapWithNulls() {
+    NullFields.NullFieldsSearchRequest request =
+        new NullFields.NullFieldsSearchRequest.Builder().setQuery(null).build();
+
+    Map<String, Object> map = request.toMap();
+    assertNull(map.get("query"));
+  }
+
+  @Test
+  public void replyToMapWithValues() {
+    NullFields.NullFieldsSearchReply reply =
+        new NullFields.NullFieldsSearchReply.Builder()
+            .setResult("result")
+            .setError("error")
+            .setIndices(Arrays.asList(1L, 2L, 3L))
+            .setRequest(new NullFields.NullFieldsSearchRequest.Builder().setQuery("hello").build())
+            .setType(NullFields.NullFieldsSearchReplyType.success)
+            .build();
+
+    Map<String, Object> map = reply.toMap();
+    assertEquals(map.get("result"), "result");
+    assertEquals(map.get("error"), "error");
+    assertEquals(map.get("indices"), Arrays.asList(1L, 2L, 3L));
+    assertEquals(map.get("request"), reply.getRequest().toMap());
+    assertEquals(map.get("type"), NullFields.NullFieldsSearchReplyType.success.ordinal());
+  }
+
+  @Test
+  public void replyToMapWithNulls() {
+    NullFields.NullFieldsSearchReply reply =
+        new NullFields.NullFieldsSearchReply.Builder()
+            .setResult(null)
+            .setError(null)
+            .setIndices(null)
+            .setRequest(null)
+            .setType(null)
+            .build();
+
+    Map<String, Object> map = reply.toMap();
+    assertNull(map.get("result"));
+    assertNull(map.get("error"));
+    assertNull(map.get("indices"));
+    assertNull(map.get("request"));
+    assertNull(map.get("type"));
+  }
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart
new file mode 100644
index 0000000..9f332d2
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart
@@ -0,0 +1,211 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Autogenerated from Pigeon (v2.0.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
+import 'package:flutter/services.dart';
+
+enum NullFieldsSearchReplyType {
+  success,
+  failure,
+}
+
+class NullFieldsSearchRequest {
+  NullFieldsSearchRequest({
+    this.query,
+  });
+
+  String? query;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['query'] = query;
+    return pigeonMap;
+  }
+
+  static NullFieldsSearchRequest decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return NullFieldsSearchRequest(
+      query: pigeonMap['query'] as String?,
+    );
+  }
+}
+
+class NullFieldsSearchReply {
+  NullFieldsSearchReply({
+    this.result,
+    this.error,
+    this.indices,
+    this.request,
+    this.type,
+  });
+
+  String? result;
+  String? error;
+  List<int?>? indices;
+  NullFieldsSearchRequest? request;
+  NullFieldsSearchReplyType? type;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['result'] = result;
+    pigeonMap['error'] = error;
+    pigeonMap['indices'] = indices;
+    pigeonMap['request'] = request == null ? null : request!.encode();
+    pigeonMap['type'] = type == null ? null : type!.index;
+    return pigeonMap;
+  }
+
+  static NullFieldsSearchReply decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return NullFieldsSearchReply(
+      result: pigeonMap['result'] as String?,
+      error: pigeonMap['error'] as String?,
+      indices: (pigeonMap['indices'] as List<Object?>?)?.cast<int?>(),
+      request: pigeonMap['request'] != null
+          ? NullFieldsSearchRequest.decode(pigeonMap['request']!)
+          : null,
+      type: pigeonMap['type'] != null
+          ? NullFieldsSearchReplyType.values[pigeonMap['type']! as int]
+          : null,
+    );
+  }
+}
+
+class _NullFieldsHostApiCodec extends StandardMessageCodec {
+  const _NullFieldsHostApiCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is NullFieldsSearchReply) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else if (value is NullFieldsSearchRequest) {
+      buffer.putUint8(129);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return NullFieldsSearchReply.decode(readValue(buffer)!);
+
+      case 129:
+        return NullFieldsSearchRequest.decode(readValue(buffer)!);
+
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+class NullFieldsHostApi {
+  /// Constructor for [NullFieldsHostApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  NullFieldsHostApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _NullFieldsHostApiCodec();
+
+  Future<NullFieldsSearchReply> search(
+      NullFieldsSearchRequest arg_nested) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.NullFieldsHostApi.search', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_nested]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyMap['result'] as NullFieldsSearchReply?)!;
+    }
+  }
+}
+
+class _NullFieldsFlutterApiCodec extends StandardMessageCodec {
+  const _NullFieldsFlutterApiCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is NullFieldsSearchReply) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else if (value is NullFieldsSearchRequest) {
+      buffer.putUint8(129);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return NullFieldsSearchReply.decode(readValue(buffer)!);
+
+      case 129:
+        return NullFieldsSearchRequest.decode(readValue(buffer)!);
+
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+abstract class NullFieldsFlutterApi {
+  static const MessageCodec<Object?> codec = _NullFieldsFlutterApiCodec();
+
+  NullFieldsSearchReply search(NullFieldsSearchRequest request);
+  static void setup(NullFieldsFlutterApi? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.NullFieldsFlutterApi.search', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.NullFieldsFlutterApi.search was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final NullFieldsSearchRequest? arg_request =
+              (args[0] as NullFieldsSearchRequest?);
+          assert(arg_request != null,
+              'Argument for dev.flutter.pigeon.NullFieldsFlutterApi.search was null, expected non-null NullFieldsSearchRequest.');
+          final NullFieldsSearchReply output = api.search(arg_request!);
+          return output;
+        });
+      }
+    }
+  }
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_fields_test.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_fields_test.dart
new file mode 100644
index 0000000..fe030a3
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_fields_test.dart
@@ -0,0 +1,160 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_unit_tests/null_fields.gen.dart';
+
+void main() {
+  test('test constructor with values', () {
+    final NullFieldsSearchRequest request =
+        NullFieldsSearchRequest(query: 'query');
+
+    final NullFieldsSearchReply reply = NullFieldsSearchReply(
+      result: 'result',
+      error: 'error',
+      indices: <int>[1, 2, 3],
+      request: request,
+      type: NullFieldsSearchReplyType.success,
+    );
+
+    expect(reply.result, 'result');
+    expect(reply.error, 'error');
+    expect(reply.indices, <int>[1, 2, 3]);
+    expect(reply.request!.query, 'query');
+    expect(reply.type, NullFieldsSearchReplyType.success);
+  });
+
+  test('test request constructor with nulls', () {
+    final NullFieldsSearchRequest request =
+        NullFieldsSearchRequest(query: null);
+
+    expect(request.query, isNull);
+  });
+
+  test('test reply constructor with nulls', () {
+    final NullFieldsSearchReply reply = NullFieldsSearchReply(
+      result: null,
+      error: null,
+      indices: null,
+      request: null,
+      type: null,
+    );
+
+    expect(reply.result, isNull);
+    expect(reply.error, isNull);
+    expect(reply.indices, isNull);
+    expect(reply.request, isNull);
+    expect(reply.type, isNull);
+  });
+
+  test('test request decode with values', () {
+    final NullFieldsSearchRequest request =
+        NullFieldsSearchRequest.decode(<String, dynamic>{
+      'query': 'query',
+    });
+
+    expect(request.query, 'query');
+  });
+
+  test('test request decode with null', () {
+    final NullFieldsSearchRequest request =
+        NullFieldsSearchRequest.decode(<String, dynamic>{
+      'query': null,
+    });
+
+    expect(request.query, isNull);
+  });
+
+  test('test reply decode with values', () {
+    final NullFieldsSearchReply reply =
+        NullFieldsSearchReply.decode(<String, dynamic>{
+      'result': 'result',
+      'error': 'error',
+      'indices': <int>[1, 2, 3],
+      'request': <String, dynamic>{
+        'query': 'query',
+      },
+      'type': NullFieldsSearchReplyType.success.index,
+    });
+
+    expect(reply.result, 'result');
+    expect(reply.error, 'error');
+    expect(reply.indices, <int>[1, 2, 3]);
+    expect(reply.request!.query, 'query');
+    expect(reply.type, NullFieldsSearchReplyType.success);
+  });
+
+  test('test reply decode with nulls', () {
+    final NullFieldsSearchReply reply =
+        NullFieldsSearchReply.decode(<String, dynamic>{
+      'result': null,
+      'error': null,
+      'indices': null,
+      'request': null,
+      'type': null,
+    });
+
+    expect(reply.result, isNull);
+    expect(reply.error, isNull);
+    expect(reply.indices, isNull);
+    expect(reply.request, isNull);
+    expect(reply.type, isNull);
+  });
+
+  test('test request encode with values', () {
+    final NullFieldsSearchRequest request =
+        NullFieldsSearchRequest(query: 'query');
+
+    expect(request.encode(), <String, dynamic>{
+      'query': 'query',
+    });
+  });
+
+  test('test request encode with null', () {
+    final NullFieldsSearchRequest request =
+        NullFieldsSearchRequest(query: null);
+
+    expect(request.encode(), <String, dynamic>{
+      'query': null,
+    });
+  });
+
+  test('test reply encode with values', () {
+    final NullFieldsSearchReply reply = NullFieldsSearchReply(
+      result: 'result',
+      error: 'error',
+      indices: <int>[1, 2, 3],
+      request: NullFieldsSearchRequest(query: 'query'),
+      type: NullFieldsSearchReplyType.success,
+    );
+
+    expect(reply.encode(), <String, dynamic>{
+      'result': 'result',
+      'error': 'error',
+      'indices': <int>[1, 2, 3],
+      'request': <String, dynamic>{
+        'query': 'query',
+      },
+      'type': NullFieldsSearchReplyType.success.index,
+    });
+  });
+
+  test('test reply encode with nulls', () {
+    final NullFieldsSearchReply reply = NullFieldsSearchReply(
+      result: null,
+      error: null,
+      indices: null,
+      request: null,
+      type: null,
+    );
+
+    expect(reply.encode(), <String, dynamic>{
+      'result': null,
+      'error': null,
+      'indices': null,
+      'request': null,
+      'type': null,
+    });
+  });
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
index 2abcbc5..94e5288 100644
--- a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
@@ -8,9 +8,9 @@
 
 /* Begin PBXBuildFile section */
 		0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */; };
+		0D21E59A27D0502D0051D07D /* background_platform_channels.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */; };
 		0D36469D27C6BE3C0069B7BF /* NullableReturnsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D36469C27C6BE3C0069B7BF /* NullableReturnsTest.m */; };
 		0D3646A027C6DCEC0069B7BF /* MockBinaryMessenger.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D36469F27C6DCEC0069B7BF /* MockBinaryMessenger.m */; };
-		0D21E59A27D0502D0051D07D /* background_platform_channels.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */; };
 		0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50127423FF75B100CD5B95 /* RunnerTests.m */; };
 		0D6FD3C526A76D400046D8BD /* primitive.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C426A76D400046D8BD /* primitive.gen.m */; };
 		0D6FD3C726A777C00046D8BD /* PrimitiveTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C626A777C00046D8BD /* PrimitiveTest.m */; };
@@ -42,6 +42,8 @@
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+		CEA7789327DE9EEB00FE0824 /* null_fields.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = CEA7789127DE9EEB00FE0824 /* null_fields.gen.m */; };
+		CEA7789527DE9F1800FE0824 /* NullFieldsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CEA7789427DE9F1800FE0824 /* NullFieldsTest.m */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -70,11 +72,11 @@
 /* Begin PBXFileReference section */
 		0D02163B27BC7B48009BD76F /* nullable_returns.gen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = nullable_returns.gen.h; sourceTree = "<group>"; };
 		0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = nullable_returns.gen.m; sourceTree = "<group>"; };
+		0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = background_platform_channels.gen.m; sourceTree = "<group>"; };
+		0D21E59927D0502D0051D07D /* background_platform_channels.gen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = background_platform_channels.gen.h; sourceTree = "<group>"; };
 		0D36469C27C6BE3C0069B7BF /* NullableReturnsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NullableReturnsTest.m; sourceTree = "<group>"; };
 		0D36469E27C6DCEC0069B7BF /* MockBinaryMessenger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockBinaryMessenger.h; sourceTree = "<group>"; };
 		0D36469F27C6DCEC0069B7BF /* MockBinaryMessenger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockBinaryMessenger.m; sourceTree = "<group>"; };
-		0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = background_platform_channels.gen.m; sourceTree = "<group>"; };
-		0D21E59927D0502D0051D07D /* background_platform_channels.gen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = background_platform_channels.gen.h; sourceTree = "<group>"; };
 		0D50127223FF75B100CD5B95 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		0D50127423FF75B100CD5B95 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
 		0D50127623FF75B100CD5B95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -131,6 +133,9 @@
 		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		CEA7789127DE9EEB00FE0824 /* null_fields.gen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = null_fields.gen.m; sourceTree = "<group>"; };
+		CEA7789227DE9EEB00FE0824 /* null_fields.gen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = null_fields.gen.h; sourceTree = "<group>"; };
+		CEA7789427DE9F1800FE0824 /* NullFieldsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NullFieldsTest.m; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -170,6 +175,7 @@
 				0DBD8C3D279B73F700E4FDBA /* NonNullFieldsTest.m */,
 				0D36469E27C6DCEC0069B7BF /* MockBinaryMessenger.h */,
 				0D36469F27C6DCEC0069B7BF /* MockBinaryMessenger.m */,
+				CEA7789427DE9F1800FE0824 /* NullFieldsTest.m */,
 			);
 			path = RunnerTests;
 			sourceTree = "<group>";
@@ -207,6 +213,8 @@
 		97C146F01CF9000F007C117D /* Runner */ = {
 			isa = PBXGroup;
 			children = (
+				CEA7789227DE9EEB00FE0824 /* null_fields.gen.h */,
+				CEA7789127DE9EEB00FE0824 /* null_fields.gen.m */,
 				0D21E59927D0502D0051D07D /* background_platform_channels.gen.h */,
 				0D21E59827D0502D0051D07D /* background_platform_channels.gen.m */,
 				0D02163B27BC7B48009BD76F /* nullable_returns.gen.h */,
@@ -396,6 +404,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CEA7789527DE9F1800FE0824 /* NullFieldsTest.m in Sources */,
 				0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */,
 				0DBD8C3E279B73F700E4FDBA /* NonNullFieldsTest.m in Sources */,
 				0DF4E5C5266ECF4A00AEA855 /* AllDatatypesTest.m in Sources */,
@@ -428,6 +437,7 @@
 				97C146F31CF9000F007C117D /* main.m in Sources */,
 				0DD2E6BC2684031300A7D764 /* host2flutter.gen.m in Sources */,
 				0DA5DFD626CC39D600D2354B /* multiple_arity.gen.m in Sources */,
+				CEA7789327DE9EEB00FE0824 /* null_fields.gen.m in Sources */,
 				0DD2E6BE2684031300A7D764 /* message.gen.m in Sources */,
 				0D21E59A27D0502D0051D07D /* background_platform_channels.gen.m in Sources */,
 				0DD2E6BA2684031300A7D764 /* void_arg_host.gen.m in Sources */,
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/NullFieldsTest.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/NullFieldsTest.m
new file mode 100644
index 0000000..7fee2dd
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/NullFieldsTest.m
@@ -0,0 +1,160 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Flutter/Flutter.h>
+#import <XCTest/XCTest.h>
+#import "EchoMessenger.h"
+#import "null_fields.gen.h"
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface NullFieldsSearchRequest ()
++ (NullFieldsSearchRequest*)fromMap:(NSDictionary*)dict;
+- (NSDictionary*)toMap;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface NullFieldsSearchReply ()
++ (NullFieldsSearchReply*)fromMap:(NSDictionary*)dict;
+- (NSDictionary*)toMap;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface NullFieldsTest : XCTestCase
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@implementation NullFieldsTest
+
+- (void)testMakeWithValues {
+  NullFieldsSearchRequest* request = [NullFieldsSearchRequest makeWithQuery:@"hello"];
+
+  NullFieldsSearchReply* reply =
+      [NullFieldsSearchReply makeWithResult:@"result"
+                                      error:@"error"
+                                    indices:@[ @1, @2, @3 ]
+                                    request:request
+                                       type:NullFieldsSearchReplyTypeSuccess];
+
+  NSArray* indices = @[ @1, @2, @3 ];
+  XCTAssertEqualObjects(@"result", reply.result);
+  XCTAssertEqualObjects(@"error", reply.error);
+  XCTAssertEqualObjects(indices, reply.indices);
+  XCTAssertEqualObjects(@"hello", reply.request.query);
+  XCTAssertEqual(NullFieldsSearchReplyTypeSuccess, reply.type);
+}
+
+- (void)testMakeRequestWithNulls {
+  NullFieldsSearchRequest* request = [NullFieldsSearchRequest makeWithQuery:nil];
+  XCTAssertNil(request.query);
+}
+
+- (void)testMakeReplyWithNulls {
+  NullFieldsSearchReply* reply =
+      [NullFieldsSearchReply makeWithResult:nil
+                                      error:nil
+                                    indices:nil
+                                    request:nil
+                                       type:NullFieldsSearchReplyTypeSuccess];
+  XCTAssertNil(reply.result);
+  XCTAssertNil(reply.error);
+  XCTAssertNil(reply.indices);
+  XCTAssertNil(reply.request);
+  XCTAssertEqual(NullFieldsSearchReplyTypeSuccess, reply.type);
+}
+
+- (void)testRequestFromMapWithValues {
+  NSDictionary* map = @{
+    @"query" : @"hello",
+  };
+  NullFieldsSearchRequest* request = [NullFieldsSearchRequest fromMap:map];
+  XCTAssertEqualObjects(@"hello", request.query);
+}
+
+- (void)testRequestFromMapWithNulls {
+  NSDictionary* map = @{
+    @"query" : [NSNull null],
+  };
+  NullFieldsSearchRequest* request = [NullFieldsSearchRequest fromMap:map];
+  XCTAssertNil(request.query);
+}
+
+- (void)testReplyFromMapWithValues {
+  NSDictionary* map = @{
+    @"result" : @"result",
+    @"error" : @"error",
+    @"indices" : @[ @1, @2, @3 ],
+    @"request" : @{
+      @"query" : @"hello",
+    },
+    @"type" : @0,
+  };
+
+  NSArray* indices = @[ @1, @2, @3 ];
+  NullFieldsSearchReply* reply = [NullFieldsSearchReply fromMap:map];
+  XCTAssertEqualObjects(@"result", reply.result);
+  XCTAssertEqualObjects(@"error", reply.error);
+  XCTAssertEqualObjects(indices, reply.indices);
+  XCTAssertEqualObjects(@"hello", reply.request.query);
+  XCTAssertEqual(NullFieldsSearchReplyTypeSuccess, reply.type);
+}
+
+- (void)testReplyFromMapWithNulls {
+  NSDictionary* map = @{
+    @"result" : [NSNull null],
+    @"error" : [NSNull null],
+    @"indices" : [NSNull null],
+    @"request" : [NSNull null],
+    @"type" : [NSNull null],
+  };
+  NullFieldsSearchReply* reply = [NullFieldsSearchReply fromMap:map];
+  XCTAssertNil(reply.result);
+  XCTAssertNil(reply.error);
+  XCTAssertNil(reply.indices);
+  XCTAssertNil(reply.request.query);
+  XCTAssertEqual(NullFieldsSearchReplyTypeSuccess, reply.type);
+}
+
+- (void)testRequestToMapWithValuess {
+  NullFieldsSearchRequest* request = [NullFieldsSearchRequest makeWithQuery:@"hello"];
+  NSDictionary* dict = [request toMap];
+  XCTAssertEqual(@"hello", dict[@"query"]);
+}
+
+- (void)testRequestToMapWithNulls {
+  NullFieldsSearchRequest* request = [NullFieldsSearchRequest makeWithQuery:nil];
+  NSDictionary* dict = [request toMap];
+  XCTAssertEqual([NSNull null], dict[@"query"]);
+}
+
+- (void)testReplyToMapWithValuess {
+  NullFieldsSearchReply* reply =
+      [NullFieldsSearchReply makeWithResult:@"result"
+                                      error:@"error"
+                                    indices:@[ @1, @2, @3 ]
+                                    request:[NullFieldsSearchRequest makeWithQuery:@"hello"]
+                                       type:NullFieldsSearchReplyTypeSuccess];
+  NSDictionary* dict = [reply toMap];
+  NSArray* indices = @[ @1, @2, @3 ];
+  XCTAssertEqualObjects(@"result", dict[@"result"]);
+  XCTAssertEqualObjects(@"error", dict[@"error"]);
+  XCTAssertEqualObjects(indices, dict[@"indices"]);
+  XCTAssertEqualObjects(@"hello", dict[@"request"][@"query"]);
+  XCTAssertEqualObjects(@0, dict[@"type"]);
+}
+
+- (void)testReplyToMapWithNulls {
+  NullFieldsSearchReply* reply =
+      [NullFieldsSearchReply makeWithResult:nil
+                                      error:nil
+                                    indices:nil
+                                    request:nil
+                                       type:NullFieldsSearchReplyTypeSuccess];
+  NSDictionary* dict = [reply toMap];
+  XCTAssertEqual([NSNull null], dict[@"result"]);
+  XCTAssertEqual([NSNull null], dict[@"error"]);
+  XCTAssertEqual([NSNull null], dict[@"indices"]);
+  XCTAssertEqual([NSNull null], dict[@"request"]);
+}
+
+@end
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 098cfac..5efd347 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
 repository: https://github.com/flutter/packages/tree/main/packages/pigeon
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 2.0.1 # This must match the version in lib/generator_tools.dart
+version: 2.0.2 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
index 91d86ed..265c027 100755
--- a/packages/pigeon/run_tests.sh
+++ b/packages/pigeon/run_tests.sh
@@ -242,6 +242,7 @@
   gen_ios_unittests_code ./pigeons/message.dart ""
   gen_ios_unittests_code ./pigeons/multiple_arity.dart ""
   gen_ios_unittests_code ./pigeons/non_null_fields.dart "NNF"
+  gen_ios_unittests_code ./pigeons/null_fields.dart ""
   gen_ios_unittests_code ./pigeons/nullable_returns.dart "NR"
   gen_ios_unittests_code ./pigeons/primitive.dart ""
   gen_ios_unittests_code ./pigeons/void_arg_flutter.dart "VAF"
@@ -303,6 +304,7 @@
   gen_android_unittests_code ./pigeons/message.dart MessagePigeon
   gen_android_unittests_code ./pigeons/multiple_arity.dart MultipleArity
   gen_android_unittests_code ./pigeons/non_null_fields.dart NonNullFields
+  gen_android_unittests_code ./pigeons/null_fields.dart NullFields
   gen_android_unittests_code ./pigeons/nullable_returns.dart NullableReturns
   gen_android_unittests_code ./pigeons/primitive.dart Primitive
   gen_android_unittests_code ./pigeons/void_arg_flutter.dart VoidArgFlutter
diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart
index f65fe58..6b54307 100644
--- a/packages/pigeon/test/java_generator_test.dart
+++ b/packages/pigeon/test/java_generator_test.dart
@@ -470,7 +470,8 @@
     expect(code, contains('public static class Outer'));
     expect(code, contains('public static class Nested'));
     expect(code, contains('private @Nullable Nested nested;'));
-    expect(code, contains('Nested.fromMap((Map)nested)'));
+    expect(code,
+        contains('(nested == null) ? null : Nested.fromMap((Map)nested)'));
     expect(code,
         contains('put("nested", (nested == null) ? null : nested.toMap());'));
   });