Merge "Fix minor error for export to framework" into androidx-main
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index cf41477..94d80452 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -20,11 +20,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
 import androidx.appsearch.app.AppSearchBlobHandle;
 import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.flags.Flags;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.appsearch.localstorage.UnlimitedLimitConfig;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 
@@ -64,16 +69,17 @@
                     "namespace", "sDocumentProperties2", SCHEMA_TYPE_2)
                     .setCreationTimestampMillis(6789L)
                     .build();
+    private static final String PREFIX = "package$databaseName/";
     private static final SchemaTypeConfigProto SCHEMA_PROTO_1 = SchemaTypeConfigProto.newBuilder()
-            .setSchemaType(SCHEMA_TYPE_1)
+            .setSchemaType(PREFIX + SCHEMA_TYPE_1)
             .build();
     private static final SchemaTypeConfigProto SCHEMA_PROTO_2 = SchemaTypeConfigProto.newBuilder()
-            .setSchemaType(SCHEMA_TYPE_2)
+            .setSchemaType(PREFIX + SCHEMA_TYPE_2)
             .build();
-    private static final String PREFIX = "package$databaseName/";
-    private static final Map<String, SchemaTypeConfigProto> SCHEMA_MAP =
-            ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, SCHEMA_PROTO_1, PREFIX + SCHEMA_TYPE_2,
-                    SCHEMA_PROTO_2);
+    private static final Map<String, Map<String, SchemaTypeConfigProto>> SCHEMA_MAP =
+            ImmutableMap.of(PREFIX, ImmutableMap.of(
+                    PREFIX + SCHEMA_TYPE_1, SCHEMA_PROTO_1,
+                    PREFIX + SCHEMA_TYPE_2, SCHEMA_PROTO_2));
 
     @Test
     public void testDocumentProtoConvert() throws Exception {
@@ -128,7 +134,8 @@
 
         GenericDocument convertedGenericDocument =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
-                        SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new SchemaCache(SCHEMA_MAP),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig()));
         DocumentProto convertedDocumentProto =
                 GenericDocumentToProtoConverter.toDocumentProto(document);
@@ -220,13 +227,15 @@
                 .addProperties(emptyDocumentListProperty)
                 .setSchemaType(PREFIX + SCHEMA_TYPE_1)
                 .build();
-        Map<String, SchemaTypeConfigProto> schemaMap =
-                ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, schemaTypeConfigProto);
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(PREFIX,
+                        ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, schemaTypeConfigProto));
 
         // Convert to the other type and check if they are matched.
         GenericDocument convertedGenericDocument =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
-                        schemaMap, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new SchemaCache(schemaMap),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig()));
         DocumentProto convertedDocumentProto =
                 GenericDocumentToProtoConverter.toDocumentProto(document);
@@ -348,14 +357,16 @@
                 .addProperties(nestedDocumentProperty)
                 .setSchemaType(PREFIX + SCHEMA_TYPE_2)
                 .build();
-        Map<String, SchemaTypeConfigProto> schemaMap =
-                ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, nestedSchemaTypeConfigProto,
-                        PREFIX + SCHEMA_TYPE_2, outerSchemaTypeConfigProto);
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(PREFIX,
+                        ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, nestedSchemaTypeConfigProto,
+                                PREFIX + SCHEMA_TYPE_2, outerSchemaTypeConfigProto));
 
         // Convert to the other type and check if they are matched.
         GenericDocument convertedGenericDocument =
                 GenericDocumentToProtoConverter.toGenericDocument(outerDocumentProto, PREFIX,
-                        schemaMap, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new SchemaCache(schemaMap),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig()));
         DocumentProto convertedDocumentProto =
                 GenericDocumentToProtoConverter.toDocumentProto(outerDocument);
@@ -365,15 +376,18 @@
     // @exportToFramework:startStrip()
     // TODO(b/274157614): setParentTypes is hidden
     @Test
+    @SuppressWarnings("deprecation")
     public void testConvertDocument_withParentTypes() throws Exception {
+        assumeFalse(Flags.enableSearchResultParentTypes());
         // Create a type with a parent type.
         SchemaTypeConfigProto schemaProto1 = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType(PREFIX + SCHEMA_TYPE_1)
                 .addParentTypes(PREFIX + SCHEMA_TYPE_2)
                 .build();
-        Map<String, SchemaTypeConfigProto> schemaMap =
-                ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, schemaProto1, PREFIX + SCHEMA_TYPE_2,
-                        SCHEMA_PROTO_2);
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(PREFIX, ImmutableMap.of(
+                        PREFIX + SCHEMA_TYPE_1, schemaProto1,
+                        PREFIX + SCHEMA_TYPE_2, SCHEMA_PROTO_2));
 
         // Create a document proto for the above type.
         DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
@@ -418,13 +432,15 @@
 
         GenericDocument actualDocWithParentAsMetaField =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
-                        schemaMap, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new SchemaCache(schemaMap),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig(),
                                 /* storeParentInfoAsSyntheticProperty= */ false,
                                 /* shouldRetrieveParentInfo= */ true));
         GenericDocument actualDocWithParentAsSyntheticProperty =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
-                        schemaMap, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new SchemaCache(schemaMap),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig(),
                                 /* storeParentInfoAsSyntheticProperty= */ true,
                                 /* shouldRetrieveParentInfo= */ true));
@@ -441,6 +457,67 @@
     // @exportToFramework:endStrip()
 
     @Test
+    public void testConvertDocument_withoutParentTypes() throws Exception {
+        assumeTrue(Flags.enableSearchResultParentTypes());
+        // Create a type with a parent type.
+        SchemaTypeConfigProto schemaProto1 = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType(PREFIX + SCHEMA_TYPE_1)
+                .addParentTypes(PREFIX + SCHEMA_TYPE_2)
+                .build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(PREFIX, ImmutableMap.of(
+                        PREFIX + SCHEMA_TYPE_1, schemaProto1,
+                        PREFIX + SCHEMA_TYPE_2, SCHEMA_PROTO_2));
+
+        // Create a document proto for the above type.
+        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
+                .setCreationTimestampMs(5L)
+                .setScore(1)
+                .setTtlMs(1L)
+                .setNamespace("namespace");
+        HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
+        propertyProtoMap.put("longKey1",
+                PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
+        propertyProtoMap.put("doubleKey1",
+                PropertyProto.newBuilder().setName("doubleKey1").addDoubleValues(1.0));
+        for (Map.Entry<String, PropertyProto.Builder> entry : propertyProtoMap.entrySet()) {
+            documentProtoBuilder.addProperties(entry.getValue());
+        }
+        DocumentProto documentProto = documentProtoBuilder.build();
+
+        // Check that the parent types list is not wrapped anywhere in GenericDocument, neither
+        // as a synthetic property nor as a meta field, since Flags.enableSearchResultParentTypes()
+        // is true.
+        GenericDocument expectedDoc =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
+                        .setCreationTimestampMillis(5L)
+                        .setScore(1)
+                        .setTtlMillis(1L)
+                        .setPropertyLong("longKey1", 1L)
+                        .setPropertyDouble("doubleKey1", 1.0)
+                        .build();
+        GenericDocument actualDoc1 =
+                GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
+                        new SchemaCache(schemaMap),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new LocalStorageIcingOptionsConfig(),
+                                /* storeParentInfoAsSyntheticProperty= */ false,
+                                /* shouldRetrieveParentInfo= */ true));
+        GenericDocument actualDoc2 =
+                GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
+                        new SchemaCache(schemaMap),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new LocalStorageIcingOptionsConfig(),
+                                /* storeParentInfoAsSyntheticProperty= */ true,
+                                /* shouldRetrieveParentInfo= */ true));
+        assertThat(actualDoc1).isEqualTo(expectedDoc);
+        assertThat(actualDoc2).isEqualTo(expectedDoc);
+    }
+
+    @Test
     public void testDocumentProtoConvert_EmbeddingProperty() throws Exception {
         GenericDocument document =
                 new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
@@ -486,7 +563,8 @@
 
         GenericDocument convertedGenericDocument =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
-                        SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new SchemaCache(SCHEMA_MAP),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig()));
         DocumentProto convertedDocumentProto =
                 GenericDocumentToProtoConverter.toDocumentProto(document);
@@ -554,7 +632,8 @@
 
         GenericDocument convertedGenericDocument =
                 GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
-                        SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new SchemaCache(SCHEMA_MAP),
+                        new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig()));
         DocumentProto convertedDocumentProto =
                 GenericDocumentToProtoConverter.toDocumentProto(document);
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
index 9ce2a32..2560643 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -25,6 +25,7 @@
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.Flags;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
 import androidx.appsearch.localstorage.SchemaCache;
@@ -34,6 +35,7 @@
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SearchResultProto;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
 import org.junit.Test;
@@ -42,27 +44,34 @@
 
 public class SearchResultToProtoConverterTest {
     @Test
+    @SuppressWarnings("deprecation")
     public void testToSearchResultProto() throws Exception {
         final String prefix =
                 "com.package.foo" + PrefixUtil.PACKAGE_DELIMITER + "databaseName"
                         + PrefixUtil.DATABASE_DELIMITER;
         final String id = "id";
         final String namespace = prefix + "namespace";
-        final String schemaType = prefix + "schema";
-        final AppSearchConfigImpl config = new AppSearchConfigImpl(new UnlimitedLimitConfig(),
-                new LocalStorageIcingOptionsConfig());
+        String schemaType = "schema";
+        String parentSchemaType = "parentSchema";
+        final String prefixedSchemaType = prefix + schemaType;
+        final String prefixedParentSchemaType = prefix + parentSchemaType;
+        final AppSearchConfigImpl config = new AppSearchConfigImpl(
+                new UnlimitedLimitConfig(),
+                new LocalStorageIcingOptionsConfig(),
+                /* storeParentInfoAsSyntheticProperty= */ false,
+                /* shouldRetrieveParentInfo= */ true);
 
         // Building the SearchResult received from query.
         DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
                 .setUri(id)
                 .setNamespace(namespace)
-                .setSchema(schemaType);
+                .setSchema(prefixedSchemaType);
 
         // A joined document
         DocumentProto.Builder joinedDocProtoBuilder = DocumentProto.newBuilder()
                 .setUri("id2")
                 .setNamespace(namespace)
-                .setSchema(schemaType);
+                .setSchema(prefixedSchemaType);
 
         SearchResultProto.ResultProto joinedResultProto = SearchResultProto.ResultProto.newBuilder()
                 .setDocument(joinedDocProtoBuilder).build();
@@ -75,31 +84,47 @@
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
                 .addResults(resultProto).build();
 
+        SchemaTypeConfigProto parentSchemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType(prefixedParentSchemaType)
+                        .build();
         SchemaTypeConfigProto schemaTypeConfigProto =
                 SchemaTypeConfigProto.newBuilder()
-                        .setSchemaType(schemaType)
+                        .addParentTypes(prefixedParentSchemaType)
+                        .setSchemaType(prefixedSchemaType)
                         .build();
         Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(prefix,
-                ImmutableMap.of(schemaType, schemaTypeConfigProto));
+                ImmutableMap.of(
+                        prefixedSchemaType, schemaTypeConfigProto,
+                        prefixedParentSchemaType, parentSchemaTypeConfigProto
+                ));
+        SchemaCache schemaCache = new SchemaCache(schemaMap);
 
         removePrefixesFromDocument(documentProtoBuilder);
         removePrefixesFromDocument(joinedDocProtoBuilder);
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
-                searchResultProto, new SchemaCache(schemaMap), config);
+                searchResultProto, schemaCache, config);
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult result = searchResultPage.getResults().get(0);
         assertThat(result.getPackageName()).isEqualTo("com.package.foo");
         assertThat(result.getDatabaseName()).isEqualTo("databaseName");
         assertThat(result.getGenericDocument()).isEqualTo(
                 GenericDocumentToProtoConverter.toGenericDocument(
-                        documentProtoBuilder.build(), prefix, schemaMap.get(prefix),
-                        config));
+                        documentProtoBuilder.build(), prefix, schemaCache, config));
 
         assertThat(result.getJoinedResults()).hasSize(1);
         assertThat(result.getJoinedResults().get(0).getGenericDocument()).isEqualTo(
                 GenericDocumentToProtoConverter.toGenericDocument(
-                        joinedDocProtoBuilder.build(), prefix, schemaMap.get(prefix),
-                        config));
+                        joinedDocProtoBuilder.build(), prefix, schemaCache, config));
+
+        if (Flags.enableSearchResultParentTypes()) {
+            assertThat(result.getParentTypeMap()).isEqualTo(
+                    ImmutableMap.of(schemaType, ImmutableList.of(parentSchemaType)));
+            assertThat(result.getGenericDocument().getParentTypes()).isNull();
+        } else {
+            assertThat(result.getParentTypeMap()).isEmpty();
+            assertThat(result.getGenericDocument().getParentTypes()).contains(parentSchemaType);
+        }
     }
 
     @Test
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
index 371e979..afc4566 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -294,7 +294,7 @@
     }
 
     @Test
-    public void testToScoringSpecProto() {
+    public void testToScoringSpecProto() throws Exception {
         String prefix = PrefixUtil.createPrefix("package", "database1");
         String schemaType = "schemaType";
         String namespace = "namespace";
@@ -328,7 +328,7 @@
     }
 
     @Test
-    public void testGenerateScoringSpecProtoWhenScorableRankingIsEnabled() {
+    public void testGenerateScoringSpecProtoWhenScorableRankingIsEnabled() throws Exception {
         String prefix1 = PrefixUtil.createPrefix("package1", "database2");
         String prefix2 = PrefixUtil.createPrefix("package2", "database1");
         String gmailSchemaType = "gmail";
@@ -508,7 +508,7 @@
     }
 
     @Test
-    public void testToResultSpecProto_projection_withJoinSpec_packageFilter() {
+    public void testToResultSpecProto_projection_withJoinSpec_packageFilter() throws Exception {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
         String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
 
@@ -618,7 +618,8 @@
     }
 
     @Test
-    public void testToResultSpecProto_projection_removeSchemaWithoutParentInFilter() {
+    public void testToResultSpecProto_projection_removeSchemaWithoutParentInFilter()
+            throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .addFilterSchemas("Person")
                 .addProjection("Artist", ImmutableList.of("name"))
@@ -671,7 +672,7 @@
     }
 
     @Test
-    public void testToSearchSpecProto_propertyFilter_withJoinSpec_packageFilter() {
+    public void testToSearchSpecProto_propertyFilter_withJoinSpec_packageFilter() throws Exception {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
         String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
 
@@ -748,7 +749,8 @@
     }
 
     @Test
-    public void testToSearchSpecProto_propertyFilter_removeSchemaWithoutParentInFilter() {
+    public void testToSearchSpecProto_propertyFilter_removeSchemaWithoutParentInFilter()
+            throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .addFilterSchemas("Person")
                 .addFilterProperties("Artist", ImmutableList.of("name"))
@@ -1370,7 +1372,7 @@
     }
 
     @Test
-    public void testGetTargetSchemaFilters_emptySearchingFilter() {
+    public void testGetTargetSchemaFilters_emptySearchingFilter() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder().build();
         String prefix1 = createPrefix("package", "database1");
         String prefix2 = createPrefix("package", "database2");
@@ -1398,7 +1400,7 @@
     }
 
     @Test
-    public void testGetTargetSchemaFilters_searchPartialFilter() {
+    public void testGetTargetSchemaFilters_searchPartialFilter() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder().build();
         String prefix1 = createPrefix("package", "database1");
         String prefix2 = createPrefix("package", "database2");
@@ -1426,7 +1428,7 @@
     }
 
     @Test
-    public void testGetTargetSchemaFilters_intersectionWithSearchingFilter() {
+    public void testGetTargetSchemaFilters_intersectionWithSearchingFilter() throws Exception {
         // Put some searching schemas.
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .addFilterSchemas("typeA", "nonExist").build();
@@ -1453,7 +1455,7 @@
     }
 
     @Test
-    public void testGetTargetSchemaFilters_polymorphismExpansion() {
+    public void testGetTargetSchemaFilters_polymorphismExpansion() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .addFilterSchemas("Person", "nonExist").build();
         String prefix = createPrefix("package", "database");
@@ -1492,7 +1494,7 @@
     }
 
     @Test
-    public void testGetTargetSchemaFilters_polymorphismExpansion_multipleLevel() {
+    public void testGetTargetSchemaFilters_polymorphismExpansion_multipleLevel() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .addFilterSchemas("A", "B").build();
         String prefix = createPrefix("package", "database");
@@ -1543,7 +1545,7 @@
     }
 
     @Test
-    public void testGetTargetSchemaFilters_intersectionWithNonExistFilter() {
+    public void testGetTargetSchemaFilters_intersectionWithNonExistFilter() throws Exception {
         // Put non-exist searching schema.
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .addFilterSchemas("nonExist").build();
@@ -1611,7 +1613,7 @@
     }
 
     @Test
-    public void testIsNothingToSearch() {
+    public void testIsNothingToSearch() throws Exception {
         String prefix = PrefixUtil.createPrefix("package", "database");
         SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
         JoinSpec joinSpec = new JoinSpec.Builder("entity")
@@ -1710,7 +1712,7 @@
     }
 
     @Test
-    public void testConvertPropertyWeights() {
+    public void testConvertPropertyWeights() throws Exception {
         String prefix1 = PrefixUtil.createPrefix("package", "database1");
         String prefix2 = PrefixUtil.createPrefix("package", "database2");
         String schemaTypeA = "typeA";
@@ -1776,7 +1778,7 @@
     }
 
     @Test
-    public void testConvertPropertyWeights_whenNoWeightsSet() {
+    public void testConvertPropertyWeights_whenNoWeightsSet() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder().build();
         String prefix1 = PrefixUtil.createPrefix("package", "database1");
         SchemaTypeConfigProto schemaTypeConfigProto =
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index 92d8c6b..444eccd 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -96,6 +96,8 @@
             case Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS:
                 // fall through
             case Features.SCHEMA_SCORABLE_PROPERTY_CONFIG:
+                // fall through
+            case Features.SEARCH_RESULT_PARENT_TYPES:
                 return true;
             default:
                 return false;
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java
index 0a8b234..0932377 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchConfig.java
@@ -33,7 +33,9 @@
     boolean shouldStoreParentInfoAsSyntheticProperty();
 
     /**
-     * Whether to include the list of parent types when returning a {@link GenericDocument}.
+     * Whether to include the list of parent types when returning a {@link GenericDocument} or a
+     * {@link androidx.appsearch.app.SearchResult} when
+     * {@link androidx.appsearch.flags.Flags#FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES} in on.
      */
     boolean shouldRetrieveParentInfo();
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 91c78e4..416d882 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -395,7 +395,7 @@
                 }
 
                 // Populate schema parent-to-children map
-                mSchemaCacheLocked.rebuildSchemaParentToChildrenMap();
+                mSchemaCacheLocked.rebuildCache();
 
                 // Populate namespace map
                 List<String> prefixedNamespaceList =
@@ -822,7 +822,7 @@
             mSchemaCacheLocked.removeFromSchemaMap(prefix, schemaType);
         }
 
-        mSchemaCacheLocked.rebuildSchemaParentToChildrenMapForPrefix(prefix);
+        mSchemaCacheLocked.rebuildCacheForPrefix(prefix);
 
         // Since the constructor of VisibilityStore will set schema. Avoid call visibility
         // store before we have already created it.
@@ -1321,10 +1321,8 @@
             DocumentProto.Builder documentBuilder = documentProto.toBuilder();
             removePrefixesFromDocument(documentBuilder);
             String prefix = createPrefix(packageName, databaseName);
-            Map<String, SchemaTypeConfigProto> schemaTypeMap =
-                    mSchemaCacheLocked.getSchemaMapForPrefix(prefix);
             return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
-                    prefix, schemaTypeMap, mConfig);
+                    prefix, mSchemaCacheLocked, mConfig);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -1363,10 +1361,8 @@
             // The schema type map cannot be null at this point. It could only be null if no
             // schema had ever been set for that prefix. Given we have retrieved a document from
             // the index, we know a schema had to have been set.
-            Map<String, SchemaTypeConfigProto> schemaTypeMap =
-                    mSchemaCacheLocked.getSchemaMapForPrefix(prefix);
             return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
-                    prefix, schemaTypeMap, mConfig);
+                    prefix, mSchemaCacheLocked, mConfig);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java
index 7138182..a2e5620 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java
@@ -18,6 +18,9 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.localstorage.util.PrefixUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -54,12 +57,23 @@
     private final Map<String, Map<String, List<String>>> mSchemaParentToChildrenMap =
             new ArrayMap<>();
 
+    /**
+     * A map that contains schema types and all parent schema types for all package-database
+     * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+     * prefixed schema type to its respective list of unprefixed parent schema types including
+     * transitive parents. It's guaranteed that child types always appear before parent types in
+     * the list.
+     */
+    private final Map<String, Map<String, List<String>>>
+            mSchemaChildToTransitiveUnprefixedParentsMap = new ArrayMap<>();
+
     public SchemaCache() {
     }
 
-    public SchemaCache(@NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+    public SchemaCache(@NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
+            throws AppSearchException {
         mSchemaMap.putAll(Preconditions.checkNotNull(schemaMap));
-        rebuildSchemaParentToChildrenMap();
+        rebuildCache();
     }
 
     /**
@@ -132,17 +146,47 @@
     }
 
     /**
-     * Rebuilds the schema parent-to-children map for the given prefix, based on the current
-     * schema map.
-     *
-     * <p>The schema parent-to-children map is required to be updated when
-     * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the
-     * results from {@link #getSchemaTypesWithDescendants} would be stale.
+     * Returns the unprefixed parent schema types, including transitive parents, for the given
+     * prefixed schema type, based on the schema child-to-parents map held in the cache. It's
+     * guaranteed that child types always appear before parent types in the list.
      */
-    public void rebuildSchemaParentToChildrenMapForPrefix(@NonNull String prefix) {
+    @NonNull
+    public List<String> getTransitiveUnprefixedParentSchemaTypes(@NonNull String prefix,
+            @NonNull String prefixedSchemaType) throws AppSearchException {
+        Preconditions.checkNotNull(prefix);
+        Preconditions.checkNotNull(prefixedSchemaType);
+
+        // If the flag is on, retrieve the parent types from the cache as it is available.
+        // Otherwise, recalculate the parent types.
+        if (Flags.enableSearchResultParentTypes()) {
+            Map<String, List<String>> unprefixedChildToParentsMap =
+                    mSchemaChildToTransitiveUnprefixedParentsMap.get(prefix);
+            if (unprefixedChildToParentsMap == null) {
+                return Collections.emptyList();
+            }
+            List<String> parents = unprefixedChildToParentsMap.get(prefixedSchemaType);
+            return parents == null ? Collections.emptyList() : parents;
+        } else {
+            return calculateTransitiveUnprefixedParentSchemaTypes(prefixedSchemaType,
+                    getSchemaMapForPrefix(prefix));
+        }
+    }
+
+    /**
+     * Rebuilds the schema parent-to-children and child-to-parents maps for the given prefix,
+     * based on the current schema map.
+     *
+     * <p>The schema parent-to-children and child-to-parents maps must be updated when
+     * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the
+     * results from {@link #getSchemaTypesWithDescendants} and
+     * {@link #getTransitiveUnprefixedParentSchemaTypes} would be stale.
+     */
+    public void rebuildCacheForPrefix(@NonNull String prefix)
+            throws AppSearchException {
         Preconditions.checkNotNull(prefix);
 
         mSchemaParentToChildrenMap.remove(prefix);
+        mSchemaChildToTransitiveUnprefixedParentsMap.remove(prefix);
         Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMap.get(prefix);
         if (prefixedSchemaMap == null) {
             return;
@@ -161,34 +205,56 @@
                 children.add(childSchemaConfig.getSchemaType());
             }
         }
-
         // Record the map for the current prefix.
         if (!parentToChildrenMap.isEmpty()) {
             mSchemaParentToChildrenMap.put(prefix, parentToChildrenMap);
         }
+
+        // If the flag is on, build the child-to-parent maps as caches. Otherwise, this
+        // information will have to be recalculated when needed.
+        if (Flags.enableSearchResultParentTypes()) {
+            // Build the child-to-parents maps for the current prefix.
+            Map<String, List<String>> childToTransitiveUnprefixedParentsMap = new ArrayMap<>();
+            for (SchemaTypeConfigProto childSchemaConfig : prefixedSchemaMap.values()) {
+                if (childSchemaConfig.getParentTypesCount() > 0) {
+                    childToTransitiveUnprefixedParentsMap.put(
+                            childSchemaConfig.getSchemaType(),
+                            calculateTransitiveUnprefixedParentSchemaTypes(
+                                    childSchemaConfig.getSchemaType(),
+                                    prefixedSchemaMap));
+                }
+            }
+            // Record the map for the current prefix.
+            if (!childToTransitiveUnprefixedParentsMap.isEmpty()) {
+                mSchemaChildToTransitiveUnprefixedParentsMap.put(prefix,
+                        childToTransitiveUnprefixedParentsMap);
+            }
+        }
     }
 
     /**
-     * Rebuilds the schema parent-to-children map based on the current schema map.
+     * Rebuilds the schema parent-to-children and child-to-parents maps based on the current
+     * schema map.
      *
-     * <p>The schema parent-to-children map is required to be updated when
+     * <p>The schema parent-to-children and child-to-parents maps must be updated when
      * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the
-     * results from {@link #getSchemaTypesWithDescendants} would be stale.
+     * results from {@link #getSchemaTypesWithDescendants} and
+     * {@link #getTransitiveUnprefixedParentSchemaTypes} would be stale.
      */
-    public void rebuildSchemaParentToChildrenMap() {
+    public void rebuildCache() throws AppSearchException {
         mSchemaParentToChildrenMap.clear();
+        mSchemaChildToTransitiveUnprefixedParentsMap.clear();
         for (String prefix : mSchemaMap.keySet()) {
-            rebuildSchemaParentToChildrenMapForPrefix(prefix);
+            rebuildCacheForPrefix(prefix);
         }
     }
 
     /**
      * Adds a schema to the schema map.
      *
-     * <p>Note that this method will invalidate the schema parent-to-children map in the cache,
-     * and either {@link #rebuildSchemaParentToChildrenMap} or
-     * {@link #rebuildSchemaParentToChildrenMapForPrefix} is required to be called to update the
-     * cache.
+     * <p>Note that this method will invalidate the schema parent-to-children and
+     * child-to-parents maps in the cache, and either {@link #rebuildCache} or
+     * {@link #rebuildCacheForPrefix} is required to be called to update the cache.
      */
     public void addToSchemaMap(@NonNull String prefix,
             @NonNull SchemaTypeConfigProto schemaTypeConfigProto) {
@@ -206,10 +272,9 @@
     /**
      * Removes a schema from the schema map.
      *
-     * <p>Note that this method will invalidate the schema parent-to-children map in the cache,
-     * and either {@link #rebuildSchemaParentToChildrenMap} or
-     * {@link #rebuildSchemaParentToChildrenMapForPrefix} is required to be called to update the
-     * cache.
+     * <p>Note that this method will invalidate the schema parent-to-children and
+     * child-to-parents maps in the cache, and either {@link #rebuildCache} or
+     * {@link #rebuildCacheForPrefix} is required to be called to update the cache.
      */
     public void removeFromSchemaMap(@NonNull String prefix, @NonNull String schemaType) {
         Preconditions.checkNotNull(prefix);
@@ -222,8 +287,8 @@
     }
 
     /**
-     * Removes the entry of the given prefix from both the schema map and the schema
-     * parent-to-children map, and returns the set of removed prefixed schema type.
+     * Removes the entry of the given prefix from the schema map, the schema parent-to-children
+     * map and the child-to-parents map, and returns the set of removed prefixed schema type.
      */
     @NonNull
     public Set<String> removePrefix(@NonNull String prefix) {
@@ -232,6 +297,7 @@
         Map<String, SchemaTypeConfigProto> removedSchemas =
                 Preconditions.checkNotNull(mSchemaMap.remove(prefix));
         mSchemaParentToChildrenMap.remove(prefix);
+        mSchemaChildToTransitiveUnprefixedParentsMap.remove(prefix);
         return removedSchemas.keySet();
     }
 
@@ -241,5 +307,65 @@
     public void clear() {
         mSchemaMap.clear();
         mSchemaParentToChildrenMap.clear();
+        mSchemaChildToTransitiveUnprefixedParentsMap.clear();
+    }
+
+    /**
+     * Get the list of unprefixed transitive parent type names of {@code prefixedSchemaType}.
+     *
+     * <p>It's guaranteed that child types always appear before parent types in the list.
+     */
+    @NonNull
+    private List<String> calculateTransitiveUnprefixedParentSchemaTypes(
+            @NonNull String prefixedSchemaType,
+            @NonNull Map<String, SchemaTypeConfigProto> prefixedSchemaMap)
+            throws AppSearchException {
+        // Please note that neither DFS nor BFS order is guaranteed to always put child types
+        // before parent types (due to the diamond problem), so a topological sorting algorithm
+        // is required.
+        Map<String, Integer> inDegreeMap = new ArrayMap<>();
+        collectParentTypeInDegrees(prefixedSchemaType, prefixedSchemaMap,
+                /* visited= */new ArraySet<>(), inDegreeMap);
+
+        List<String> result = new ArrayList<>();
+        Queue<String> queue = new ArrayDeque<>();
+        // prefixedSchemaType is the only type that has zero in-degree at this point.
+        queue.add(prefixedSchemaType);
+        while (!queue.isEmpty()) {
+            SchemaTypeConfigProto currentSchema = Preconditions.checkNotNull(
+                    prefixedSchemaMap.get(queue.poll()));
+            for (int i = 0; i < currentSchema.getParentTypesCount(); ++i) {
+                String prefixedParentType = currentSchema.getParentTypes(i);
+                int parentInDegree =
+                        Preconditions.checkNotNull(inDegreeMap.get(prefixedParentType)) - 1;
+                inDegreeMap.put(prefixedParentType, parentInDegree);
+                if (parentInDegree == 0) {
+                    result.add(PrefixUtil.removePrefix(prefixedParentType));
+                    queue.add(prefixedParentType);
+                }
+            }
+        }
+        return result;
+    }
+
+    private void collectParentTypeInDegrees(
+            @NonNull String prefixedSchemaType,
+            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap,
+            @NonNull Set<String> visited, @NonNull Map<String, Integer> inDegreeMap) {
+        if (visited.contains(prefixedSchemaType)) {
+            return;
+        }
+        visited.add(prefixedSchemaType);
+        SchemaTypeConfigProto schema =
+                Preconditions.checkNotNull(schemaTypeMap.get(prefixedSchemaType));
+        for (int i = 0; i < schema.getParentTypesCount(); ++i) {
+            String prefixedParentType = schema.getParentTypes(i);
+            Integer parentInDegree = inDegreeMap.get(prefixedParentType);
+            if (parentInDegree == null) {
+                parentInDegree = 0;
+            }
+            inDegreeMap.put(prefixedParentType, parentInDegree + 1);
+            collectParentTypeInDegrees(prefixedParentType, schemaTypeMap, visited, inDegreeMap);
+        }
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index 726e5d4..67cfbf0 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -25,10 +25,9 @@
 import androidx.appsearch.app.ExperimentalAppSearchApi;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.Flags;
 import androidx.appsearch.localstorage.AppSearchConfig;
-import androidx.appsearch.localstorage.util.PrefixUtil;
-import androidx.collection.ArrayMap;
-import androidx.collection.ArraySet;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.DocumentProto;
@@ -37,13 +36,10 @@
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.protobuf.ByteString;
 
-import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Queue;
-import java.util.Set;
 
 /**
  * Translates a {@link GenericDocument} into a {@link DocumentProto}.
@@ -153,17 +149,22 @@
      *                      document proto should have its package + database prefix stripped
      *                      from its fields.
      * @param prefix        the package + database prefix used searching the {@code schemaTypeMap}.
-     * @param schemaTypeMap map of prefixed schema type to {@link SchemaTypeConfigProto}, used
-     *                      for looking up the default empty value to set for a document property
-     *                      that has all empty values.
+     * @param schemaCache   The SchemaCache instance held in AppSearch.
      */
     @NonNull
+    @SuppressWarnings("deprecation")
     @OptIn(markerClass = ExperimentalAppSearchApi.class)
     public static GenericDocument toGenericDocument(@NonNull DocumentProtoOrBuilder proto,
             @NonNull String prefix,
-            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull AppSearchConfig config) throws AppSearchException {
         Preconditions.checkNotNull(proto);
+        Preconditions.checkNotNull(prefix);
+        Preconditions.checkNotNull(schemaCache);
+        Preconditions.checkNotNull(config);
+        Map<String, SchemaTypeConfigProto> schemaTypeMap =
+                schemaCache.getSchemaMapForPrefix(prefix);
+
         GenericDocument.Builder<?> documentBuilder =
                 new GenericDocument.Builder<>(proto.getNamespace(), proto.getUri(),
                         proto.getSchema())
@@ -171,9 +172,10 @@
                         .setTtlMillis(proto.getTtlMs())
                         .setCreationTimestampMillis(proto.getCreationTimestampMs());
         String prefixedSchemaType = prefix + proto.getSchema();
-        if (config.shouldRetrieveParentInfo()) {
+        if (config.shouldRetrieveParentInfo() && !Flags.enableSearchResultParentTypes()) {
             List<String> parentSchemaTypes =
-                    getUnprefixedParentSchemaTypes(prefixedSchemaType, schemaTypeMap);
+                    schemaCache.getTransitiveUnprefixedParentSchemaTypes(
+                            prefix, prefixedSchemaType);
             if (!parentSchemaTypes.isEmpty()) {
                 if (config.shouldStoreParentInfoAsSyntheticProperty()) {
                     documentBuilder.setPropertyString(
@@ -222,7 +224,7 @@
                 GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
                 for (int j = 0; j < values.length; j++) {
                     values[j] = toGenericDocument(property.getDocumentValues(j), prefix,
-                            schemaTypeMap, config);
+                            schemaCache, config);
                 }
                 documentBuilder.setPropertyDocument(name, values);
             } else if (property.getVectorValuesCount() > 0) {
@@ -281,66 +283,6 @@
         return builder.build();
     }
 
-    /**
-     * Get the list of unprefixed parent type names of {@code prefixedSchemaType}.
-     *
-     * <p>It's guaranteed that child types always appear before parent types in the list.
-     */
-    // TODO(b/290389974): Consider caching the result based prefixedSchemaType, and reset the
-    //  cache whenever a new setSchema is called.
-    @NonNull
-    private static List<String> getUnprefixedParentSchemaTypes(
-            @NonNull String prefixedSchemaType,
-            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap) throws AppSearchException {
-        // Please note that neither DFS nor BFS order is guaranteed to always put child types
-        // before parent types (due to the diamond problem), so a topological sorting algorithm
-        // is required.
-        Map<String, Integer> inDegreeMap = new ArrayMap<>();
-        collectParentTypeInDegrees(prefixedSchemaType, schemaTypeMap,
-                /* visited= */new ArraySet<>(), inDegreeMap);
-
-        List<String> result = new ArrayList<>();
-        Queue<String> queue = new ArrayDeque<>();
-        // prefixedSchemaType is the only type that has zero in-degree at this point.
-        queue.add(prefixedSchemaType);
-        while (!queue.isEmpty()) {
-            SchemaTypeConfigProto currentSchema = Preconditions.checkNotNull(
-                    schemaTypeMap.get(queue.poll()));
-            for (int i = 0; i < currentSchema.getParentTypesCount(); ++i) {
-                String prefixedParentType = currentSchema.getParentTypes(i);
-                int parentInDegree =
-                        Preconditions.checkNotNull(inDegreeMap.get(prefixedParentType)) - 1;
-                inDegreeMap.put(prefixedParentType, parentInDegree);
-                if (parentInDegree == 0) {
-                    result.add(PrefixUtil.removePrefix(prefixedParentType));
-                    queue.add(prefixedParentType);
-                }
-            }
-        }
-        return result;
-    }
-
-    private static void collectParentTypeInDegrees(
-            @NonNull String prefixedSchemaType,
-            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap,
-            @NonNull Set<String> visited, @NonNull Map<String, Integer> inDegreeMap) {
-        if (visited.contains(prefixedSchemaType)) {
-            return;
-        }
-        visited.add(prefixedSchemaType);
-        SchemaTypeConfigProto schema =
-                Preconditions.checkNotNull(schemaTypeMap.get(prefixedSchemaType));
-        for (int i = 0; i < schema.getParentTypesCount(); ++i) {
-            String prefixedParentType = schema.getParentTypes(i);
-            Integer parentInDegree = inDegreeMap.get(prefixedParentType);
-            if (parentInDegree == null) {
-                parentInDegree = 0;
-            }
-            inDegreeMap.put(prefixedParentType, parentInDegree + 1);
-            collectParentTypeInDegrees(prefixedParentType, schemaTypeMap, visited, inDegreeMap);
-        }
-    }
-
     private static void setEmptyProperty(@NonNull String propertyName,
             @NonNull GenericDocument.Builder<?> documentBuilder,
             @NonNull SchemaTypeConfigProto schema) {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index b88eccd..bae4f74 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -21,17 +21,22 @@
 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.Flags;
 import androidx.appsearch.localstorage.AppSearchConfig;
 import androidx.appsearch.localstorage.SchemaCache;
+import androidx.collection.ArrayMap;
 
 import com.google.android.icing.proto.DocumentProto;
-import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.DocumentProtoOrBuilder;
+import com.google.android.icing.proto.PropertyProto;
 import com.google.android.icing.proto.SearchResultProto;
 import com.google.android.icing.proto.SnippetMatchProto;
 import com.google.android.icing.proto.SnippetProto;
@@ -79,6 +84,7 @@
      * @return A {@link SearchResult}.
      */
     @NonNull
+    @OptIn(markerClass = ExperimentalAppSearchApi.class)
     private static SearchResult toUnprefixedSearchResult(
             @NonNull SearchResultProto.ResultProto proto,
             @NonNull SchemaCache schemaCache,
@@ -86,11 +92,9 @@
 
         DocumentProto.Builder documentBuilder = proto.getDocument().toBuilder();
         String prefix = removePrefixesFromDocument(documentBuilder);
-        Map<String, SchemaTypeConfigProto> schemaTypeMap =
-                schemaCache.getSchemaMapForPrefix(prefix);
         GenericDocument document =
                 GenericDocumentToProtoConverter.toGenericDocument(documentBuilder, prefix,
-                        schemaTypeMap, config);
+                        schemaCache, config);
         SearchResult.Builder builder =
                 new SearchResult.Builder(getPackageName(prefix), getDatabaseName(prefix))
                         .setGenericDocument(document).setRankingSignal(proto.getScore());
@@ -118,9 +122,36 @@
             builder.addJoinedResult(
                     toUnprefixedSearchResult(joinedResultProto, schemaCache, config));
         }
+        if (config.shouldRetrieveParentInfo() && Flags.enableSearchResultParentTypes()) {
+            Map<String, List<String>> parentTypeMap = new ArrayMap<>();
+            collectParentTypeMap(documentBuilder, prefix, schemaCache, parentTypeMap);
+            builder.setParentTypeMap(parentTypeMap);
+        }
         return builder.build();
     }
 
+    private static void collectParentTypeMap(
+            @NonNull DocumentProtoOrBuilder proto,
+            @NonNull String prefix,
+            @NonNull SchemaCache schemaCache,
+            @NonNull Map<String, List<String>> parentTypeMap) throws AppSearchException {
+        if (!parentTypeMap.containsKey(proto.getSchema())) {
+            List<String> parentSchemaTypes = schemaCache.getTransitiveUnprefixedParentSchemaTypes(
+                    prefix, prefix + proto.getSchema());
+            if (!parentSchemaTypes.isEmpty()) {
+                parentTypeMap.put(proto.getSchema(), parentSchemaTypes);
+            }
+        }
+        // Handling nested documents
+        for (int i = 0; i < proto.getPropertiesCount(); i++) {
+            PropertyProto property = proto.getProperties(i);
+            for (int j = 0; j < property.getDocumentValuesCount(); j++) {
+                collectParentTypeMap(property.getDocumentValues(j), prefix, schemaCache,
+                        parentTypeMap);
+            }
+        }
+    }
+
     private static SearchResult.MatchInfo toMatchInfo(
             @NonNull SnippetMatchProto snippetMatchProto, @NonNull String propertyPath) {
         int exactMatchPosition = snippetMatchProto.getExactMatchUtf16Position();
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index 1caea0c..8623198 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -142,6 +142,9 @@
             case Features.SCHEMA_SCORABLE_PROPERTY_CONFIG:
                 // TODO(b/357105837) : Update when feature is ready in service-appsearch.
                 // fall through
+            case Features.SEARCH_RESULT_PARENT_TYPES:
+                // TODO(b/371610934) : Update when feature is ready in service-appsearch.
+                // fall through
             default:
                 return false;
         }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
index 55d5045..89216ba 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
@@ -115,6 +115,7 @@
      * {@link androidx.appsearch.app.GenericDocument}.
      */
     @NonNull
+    @SuppressWarnings("deprecation")
     public static GenericDocument toJetpackGenericDocument(
             @NonNull android.app.appsearch.GenericDocument platformDocument) {
         Preconditions.checkNotNull(platformDocument);
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
index 9817784..b4e9df0 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
@@ -60,6 +60,8 @@
                 builder.addJoinedResult(toJetpackSearchResult(joinedResult));
             }
         }
+        // TODO(b/332642571): Add informational ranking signal once it is available in platform.
+        // TODO(b/371610934): Set parentTypeMap once it is available in platform.
         return builder.build();
     }
 
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
index 00be5f8..512b995 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
@@ -93,6 +93,8 @@
                 // fall through
             case Features.SCHEMA_SCORABLE_PROPERTY_CONFIG:
                 // fall through
+            case Features.SEARCH_RESULT_PARENT_TYPES:
+                // fall through
             default:
                 return false; // AppSearch features absent in GMSCore AppSearch.
         }
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 9a69351..0930ef8 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -330,13 +330,19 @@
   }
 
   public interface DocumentClassFactory<T> {
-    method public T fromGenericDocument(androidx.appsearch.app.GenericDocument, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?) throws androidx.appsearch.exceptions.AppSearchException;
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public T fromGenericDocument(androidx.appsearch.app.GenericDocument, androidx.appsearch.app.DocumentClassMappingContext) throws androidx.appsearch.exceptions.AppSearchException;
     method public java.util.List<java.lang.Class<? extends java.lang.Object!>!> getDependencyDocumentClasses() throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
     method public String getSchemaName();
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  public class DocumentClassMappingContext {
+    ctor @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public DocumentClassMappingContext(java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?);
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getDocumentClassMap();
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getParentTypeMap();
+  }
+
   @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {
     ctor public EmbeddingVector(float[], String);
     method public String getModelSignature();
@@ -373,6 +379,7 @@
     field @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public static final String SCHEMA_SCORABLE_PROPERTY_CONFIG = "SCHEMA_SCORABLE_PROPERTY_CONFIG";
     field @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public static final String SEARCH_RESULT_PARENT_TYPES = "SEARCH_RESULT_PARENT_TYPES";
     field @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public static final String SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS = "SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS";
     field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
     field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
@@ -417,7 +424,7 @@
     method public int getScore();
     method public long getTtlMillis();
     method public <T> T toDocumentClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public <T> T toDocumentClass(Class<T!>, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?) throws androidx.appsearch.exceptions.AppSearchException;
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public <T> T toDocumentClass(Class<T!>, androidx.appsearch.app.DocumentClassMappingContext) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
@@ -633,6 +640,7 @@
     method public java.util.List<androidx.appsearch.app.SearchResult!> getJoinedResults();
     method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
     method public String getPackageName();
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getParentTypeMap();
     method public double getRankingSignal();
   }
 
@@ -644,6 +652,7 @@
     method public androidx.appsearch.app.SearchResult build();
     method public androidx.appsearch.app.SearchResult.Builder setDocument(Object) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchResult.Builder setGenericDocument(androidx.appsearch.app.GenericDocument);
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.SearchResult.Builder setParentTypeMap(java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>);
     method public androidx.appsearch.app.SearchResult.Builder setRankingSignal(double);
   }
 
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 9a69351..0930ef8 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -330,13 +330,19 @@
   }
 
   public interface DocumentClassFactory<T> {
-    method public T fromGenericDocument(androidx.appsearch.app.GenericDocument, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?) throws androidx.appsearch.exceptions.AppSearchException;
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public T fromGenericDocument(androidx.appsearch.app.GenericDocument, androidx.appsearch.app.DocumentClassMappingContext) throws androidx.appsearch.exceptions.AppSearchException;
     method public java.util.List<java.lang.Class<? extends java.lang.Object!>!> getDependencyDocumentClasses() throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
     method public String getSchemaName();
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  public class DocumentClassMappingContext {
+    ctor @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public DocumentClassMappingContext(java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?);
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getDocumentClassMap();
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getParentTypeMap();
+  }
+
   @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {
     ctor public EmbeddingVector(float[], String);
     method public String getModelSignature();
@@ -373,6 +379,7 @@
     field @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public static final String SCHEMA_SCORABLE_PROPERTY_CONFIG = "SCHEMA_SCORABLE_PROPERTY_CONFIG";
     field @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public static final String SEARCH_RESULT_PARENT_TYPES = "SEARCH_RESULT_PARENT_TYPES";
     field @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public static final String SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS = "SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS";
     field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
     field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
@@ -417,7 +424,7 @@
     method public int getScore();
     method public long getTtlMillis();
     method public <T> T toDocumentClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public <T> T toDocumentClass(Class<T!>, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?) throws androidx.appsearch.exceptions.AppSearchException;
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public <T> T toDocumentClass(Class<T!>, androidx.appsearch.app.DocumentClassMappingContext) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
@@ -633,6 +640,7 @@
     method public java.util.List<androidx.appsearch.app.SearchResult!> getJoinedResults();
     method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
     method public String getPackageName();
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getParentTypeMap();
     method public double getRankingSignal();
   }
 
@@ -644,6 +652,7 @@
     method public androidx.appsearch.app.SearchResult build();
     method public androidx.appsearch.app.SearchResult.Builder setDocument(Object) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchResult.Builder setGenericDocument(androidx.appsearch.app.GenericDocument);
+    method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.SearchResult.Builder setParentTypeMap(java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>);
     method public androidx.appsearch.app.SearchResult.Builder setRankingSignal(double);
   }
 
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
index 23cdf04..77b032b 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -43,6 +43,8 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.ListenableFuture;
 
 import org.junit.After;
@@ -1703,6 +1705,7 @@
     @Test
     public void testPolymorphismForInterface() throws Exception {
         assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
 
         mSession.setSchemaAsync(new SetSchemaRequest.Builder()
                 // Adding BusinessImpl should be enough to add all the dependency classes.
@@ -1720,9 +1723,7 @@
         assertThat(rootGeneric.getSchemaType()).isEqualTo("InterfaceRoot");
 
         Place place = Place.createPlace("id1", "namespace", 2000, "place_loc");
-        GenericDocument placeGeneric =
-                new GenericDocument.Builder<>(GenericDocument.fromDocumentClass(place))
-                        .setParentTypes(Collections.singletonList("InterfaceRoot")).build();
+        GenericDocument placeGeneric = GenericDocument.fromDocumentClass(place);
         assertThat(placeGeneric.getId()).isEqualTo("id1");
         assertThat(placeGeneric.getNamespace()).isEqualTo("namespace");
         assertThat(placeGeneric.getCreationTimestampMillis()).isEqualTo(2000);
@@ -1735,9 +1736,7 @@
                 .setCreationTimestamp(3000)
                 .setOrganizationDescription("organization_dec")
                 .build();
-        GenericDocument organizationGeneric =
-                new GenericDocument.Builder<>(GenericDocument.fromDocumentClass(organization))
-                        .setParentTypes(Collections.singletonList("InterfaceRoot")).build();
+        GenericDocument organizationGeneric = GenericDocument.fromDocumentClass(organization);
         assertThat(organizationGeneric.getId()).isEqualTo("id2");
         assertThat(organizationGeneric.getNamespace()).isEqualTo("namespace");
         assertThat(organizationGeneric.getCreationTimestampMillis()).isEqualTo(3000);
@@ -1749,9 +1748,7 @@
                 "business_dec", "business_name");
         // At runtime, business is type of BusinessImpl. As a result, the list of parent types
         // for it should contain Business.
-        GenericDocument businessGeneric = new GenericDocument.Builder<>(
-                GenericDocument.fromDocumentClass(business)).setParentTypes(new ArrayList<>(
-                Arrays.asList("Business", "Place", "Organization", "InterfaceRoot"))).build();
+        GenericDocument businessGeneric = GenericDocument.fromDocumentClass(business);
         assertThat(businessGeneric.getId()).isEqualTo("id3");
         assertThat(businessGeneric.getNamespace()).isEqualTo("namespace");
         assertThat(businessGeneric.getCreationTimestampMillis()).isEqualTo(4000);
@@ -2400,7 +2397,8 @@
         // Test that even when deserializing genericDocument to InterfaceRoot, we will get a
         // Person instance, instead of just an InterfaceRoot.
         InterfaceRoot interfaceRoot = genericDocument.toDocumentClass(InterfaceRoot.class,
-                AppSearchDocumentClassMap.getGlobalMap());
+                new DocumentClassMappingContext(
+                        AppSearchDocumentClassMap.getGlobalMap(), /* parentTypeMap= */null));
         assertThat(interfaceRoot).isInstanceOf(Person.class);
         Person newPerson = (Person) interfaceRoot;
         assertThat(newPerson.getId()).isEqualTo("id");
@@ -2432,7 +2430,8 @@
         // Without parent information, toDocumentClass() will try to deserialize unknown type to
         // the type that is specified in the parameter.
         InterfaceRoot interfaceRoot = genericDocument.toDocumentClass(InterfaceRoot.class,
-                AppSearchDocumentClassMap.getGlobalMap());
+                new DocumentClassMappingContext(
+                        AppSearchDocumentClassMap.getGlobalMap(), /* parentTypeMap= */null));
         assertThat(interfaceRoot).isNotInstanceOf(Person.class);
         assertThat(interfaceRoot).isInstanceOf(InterfaceRoot.class);
         assertThat(interfaceRoot.getId()).isEqualTo("id");
@@ -2441,11 +2440,10 @@
 
         // With parent information, toDocumentClass() will try to deserialize unknown type to the
         // nearest known parent type.
-        genericDocument = new GenericDocument.Builder<>(genericDocument)
-                .setParentTypes(new ArrayList<>(Arrays.asList("Person", "InterfaceRoot")))
-                .build();
         interfaceRoot = genericDocument.toDocumentClass(InterfaceRoot.class,
-                AppSearchDocumentClassMap.getGlobalMap());
+                new DocumentClassMappingContext(AppSearchDocumentClassMap.getGlobalMap(),
+                        ImmutableMap.of("UnknownType",
+                                ImmutableList.of("Person", "InterfaceRoot"))));
         assertThat(interfaceRoot).isInstanceOf(Person.class);
         Person newPerson = (Person) interfaceRoot;
         assertThat(newPerson.getId()).isEqualTo("id");
@@ -2456,7 +2454,7 @@
     }
 
     @Test
-    public void testPolymorphicDeserialization_nestedType() throws Exception {
+    public void testPolymorphicDeserialization_NestedType() throws Exception {
         // Create a Person document
         Person.Builder personBuilder = new Person.Builder("id_person", "namespace")
                 .setCreationTimestamp(3000)
@@ -2478,7 +2476,9 @@
         // Test that when deserializing genericDocument, we will get nested Person and Place
         // instances, instead of just nested InterfaceRoot instances.
         DocumentCollection newDocumentCollection = genericDocument.toDocumentClass(
-                DocumentCollection.class, AppSearchDocumentClassMap.getGlobalMap());
+                DocumentCollection.class,
+                new DocumentClassMappingContext(
+                        AppSearchDocumentClassMap.getGlobalMap(), /* parentTypeMap= */null));
         assertThat(newDocumentCollection.mId).isEqualTo("id_collection");
         assertThat(newDocumentCollection.mNamespace).isEqualTo("namespace");
         assertThat(newDocumentCollection.mCollection).hasLength(2);
@@ -2519,10 +2519,12 @@
     }
 
     @Test
-    public void testPolymorphicDeserialization_Integration() throws Exception {
+    @SuppressWarnings("deprecation")
+    public void testPolymorphicDeserialization_Integration()
+            throws Exception {
         assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
 
-        // Add an unknown business type this is a subtype of Business.
+        // Add an unknown business type that is a subtype of Business.
         mSession.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addDocumentClasses(Business.class)
                 .addSchemas(new AppSearchSchema.Builder("UnknownBusiness")
@@ -2556,20 +2558,29 @@
                         .build();
         checkIsBatchResultSuccess(mSession.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(genericDoc).build()));
+        GenericDocument expectedGenericDoc;
+        if (mSession.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)) {
+            // When SearchResult wraps parent information, GenericDocument should not do.
+            expectedGenericDoc = genericDoc;
+        } else {
+            // When SearchResult does not wrap parent information, GenericDocument should do.
+            expectedGenericDoc = new GenericDocument.Builder<>(genericDoc)
+                    .setParentTypes(
+                            new ArrayList<>(Arrays.asList("Business", "Place", "Organization",
+                                    "InterfaceRoot")))
+                    .build();
+        }
 
         // Query to get the document back, with parent information added.
-        SearchResults searchResults = mSession.search("", new SearchSpec.Builder().build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        GenericDocument actualGenericDoc = documents.get(0);
-        GenericDocument expectedGenericDoc = new GenericDocument.Builder<>(genericDoc)
-                .setParentTypes(new ArrayList<>(Arrays.asList("Business", "Place", "Organization",
-                        "InterfaceRoot")))
-                .build();
-        assertThat(actualGenericDoc).isEqualTo(expectedGenericDoc);
+        List<SearchResult> searchResults = retrieveAllSearchResults(
+                mSession.search("", new SearchSpec.Builder().build())
+        );
+        assertThat(searchResults).hasSize(1);
+        SearchResult result = searchResults.get(0);
+        assertThat(result.getGenericDocument()).isEqualTo(expectedGenericDoc);
 
         // Deserializing it to InterfaceRoot will get a Business instance back.
-        InterfaceRoot interfaceRoot = actualGenericDoc.toDocumentClass(InterfaceRoot.class,
+        InterfaceRoot interfaceRoot = result.getDocument(InterfaceRoot.class,
                 AppSearchDocumentClassMap.getGlobalMap());
         assertThat(interfaceRoot).isInstanceOf(Business.class);
         Business business = (Business) interfaceRoot;
@@ -2581,6 +2592,95 @@
         assertThat(business.getBusinessName()).isEqualTo("business_name");
     }
 
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testPolymorphicDeserialization_NestedType_Integration() throws Exception {
+        assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+
+        // Add an unknown business type that is a subtype of Business, and a DocumentCollection
+        // type that can hold any nested InterfaceRoot document.
+        mSession.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addDocumentClasses(Business.class)
+                .addSchemas(new AppSearchSchema.Builder("UnknownBusiness")
+                        .addParentType("Business")
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "organizationDescription")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("location")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "businessName")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "unknownProperty")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .build())
+                .addDocumentClasses(DocumentCollection.class)
+                .build()).get();
+        // Create and put a DocumentCollection that includes an UnknownBusiness document.
+        GenericDocument unknownBusinessGenericDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "UnknownBusiness")
+                        .setCreationTimestampMillis(3000)
+                        .setPropertyString("location", "business_loc")
+                        .setPropertyString("organizationDescription", "business_dec")
+                        .setPropertyString("businessName", "business_name")
+                        .setPropertyString("unknownProperty", "foo")
+                        .build();
+        GenericDocument documentCollectionGenericDoc = new GenericDocument.Builder<>(
+                "namespace", "id2", "DocumentCollection")
+                .setCreationTimestampMillis(3000)
+                .setPropertyDocument("collection", unknownBusinessGenericDoc)
+                .build();
+        checkIsBatchResultSuccess(mSession.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(
+                        documentCollectionGenericDoc).build()));
+        GenericDocument expectedDocumentCollectionGenericDoc;
+        if (mSession.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)) {
+            // When SearchResult wraps parent information, GenericDocument should not do.
+            expectedDocumentCollectionGenericDoc = documentCollectionGenericDoc;
+        } else {
+            // When SearchResult does not wrap parent information, GenericDocument should do.
+            GenericDocument expectedUnknownBusinessGenericDoc = new GenericDocument.Builder<>(
+                    unknownBusinessGenericDoc)
+                    .setParentTypes(
+                            new ArrayList<>(Arrays.asList("Business", "Place", "Organization",
+                                    "InterfaceRoot")))
+                    .build();
+            expectedDocumentCollectionGenericDoc = new GenericDocument.Builder<>(
+                    "namespace", "id2", "DocumentCollection")
+                    .setCreationTimestampMillis(3000)
+                    .setPropertyDocument("collection", expectedUnknownBusinessGenericDoc)
+                    .build();
+        }
+
+        // Query to get the document back, with parent information added.
+        List<SearchResult> searchResults = retrieveAllSearchResults(
+                mSession.search("", new SearchSpec.Builder().build())
+        );
+        assertThat(searchResults).hasSize(1);
+        SearchResult result = searchResults.get(0);
+        assertThat(result.getGenericDocument()).isEqualTo(expectedDocumentCollectionGenericDoc);
+
+        // Deserialize documentCollectionGenericDoc and check that it includes a Business
+        // instance, instead of an InterfaceRoot instance.
+        DocumentCollection documentCollection = result.getDocument(DocumentCollection.class,
+                AppSearchDocumentClassMap.getGlobalMap());
+        assertThat(documentCollection.mCollection).asList().hasSize(1);
+        assertThat(documentCollection.mCollection[0]).isInstanceOf(Business.class);
+        Business business = (Business) documentCollection.mCollection[0];
+        assertThat(business.getId()).isEqualTo("id1");
+        assertThat(business.getNamespace()).isEqualTo("namespace");
+        assertThat(business.getCreationTimestamp()).isEqualTo(3000);
+        assertThat(business.getLocation()).isEqualTo("business_loc");
+        assertThat(business.getOrganizationDescription()).isEqualTo("business_dec");
+        assertThat(business.getBusinessName()).isEqualTo("business_name");
+    }
+
+
     // InterfaceRoot
     //   |    \
     //   |    Person
@@ -2589,6 +2689,7 @@
     @Test
     public void testPolymorphicDeserialization_IntegrationDiamondThreeTypes() throws Exception {
         assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
 
         mSession.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(new AppSearchSchema.Builder("UnknownA")
@@ -2617,17 +2718,15 @@
                 new PutDocumentsRequest.Builder().addGenericDocuments(genericDoc).build()));
 
         // Query to get the document back, with parent information added.
-        SearchResults searchResults = mSession.search("", new SearchSpec.Builder().build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        GenericDocument actualGenericDoc = documents.get(0);
-        GenericDocument expectedGenericDoc = new GenericDocument.Builder<>(genericDoc)
-                .setParentTypes(new ArrayList<>(Arrays.asList("Person", "InterfaceRoot")))
-                .build();
-        assertThat(actualGenericDoc).isEqualTo(expectedGenericDoc);
+        List<SearchResult> searchResults = retrieveAllSearchResults(
+                mSession.search("", new SearchSpec.Builder().build())
+        );
+        assertThat(searchResults).hasSize(1);
+        SearchResult result = searchResults.get(0);
+        assertThat(result.getGenericDocument()).isEqualTo(genericDoc);
 
         // Deserializing it to InterfaceRoot will get a Person instance back.
-        InterfaceRoot interfaceRoot = actualGenericDoc.toDocumentClass(InterfaceRoot.class,
+        InterfaceRoot interfaceRoot = result.getDocument(InterfaceRoot.class,
                 AppSearchDocumentClassMap.getGlobalMap());
         assertThat(interfaceRoot).isInstanceOf(Person.class);
         Person person = (Person) interfaceRoot;
@@ -2646,6 +2745,7 @@
     @Test
     public void testPolymorphicDeserialization_IntegrationDiamondTwoUnknown() throws Exception {
         assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
 
         mSession.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(new AppSearchSchema.Builder("UnknownA")
@@ -2677,18 +2777,15 @@
                 new PutDocumentsRequest.Builder().addGenericDocuments(genericDoc).build()));
 
         // Query to get the document back, with parent information added.
-        SearchResults searchResults = mSession.search("", new SearchSpec.Builder().build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        GenericDocument actualGenericDoc = documents.get(0);
-        GenericDocument expectedGenericDoc = new GenericDocument.Builder<>(genericDoc)
-                .setParentTypes(new ArrayList<>(
-                        Arrays.asList("UnknownA", "Person", "InterfaceRoot")))
-                .build();
-        assertThat(actualGenericDoc).isEqualTo(expectedGenericDoc);
+        List<SearchResult> searchResults = retrieveAllSearchResults(
+                mSession.search("", new SearchSpec.Builder().build())
+        );
+        assertThat(searchResults).hasSize(1);
+        SearchResult result = searchResults.get(0);
+        assertThat(result.getGenericDocument()).isEqualTo(genericDoc);
 
         // Deserializing it to InterfaceRoot will get a Person instance back.
-        InterfaceRoot interfaceRoot = actualGenericDoc.toDocumentClass(InterfaceRoot.class,
+        InterfaceRoot interfaceRoot = result.getDocument(InterfaceRoot.class,
                 AppSearchDocumentClassMap.getGlobalMap());
         assertThat(interfaceRoot).isInstanceOf(Person.class);
         Person person = (Person) interfaceRoot;
@@ -2707,6 +2804,7 @@
     @Test
     public void testPolymorphicDeserialization_IntegrationDiamondOneUnknown() throws Exception {
         assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mSession.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
 
         mSession.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addDocumentClasses(Person.class)
@@ -2742,19 +2840,16 @@
                 new PutDocumentsRequest.Builder().addGenericDocuments(genericDoc).build()));
 
         // Query to get the document back, with parent information added.
-        SearchResults searchResults = mSession.search("", new SearchSpec.Builder().build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        GenericDocument actualGenericDoc = documents.get(0);
-        GenericDocument expectedGenericDoc = new GenericDocument.Builder<>(genericDoc)
-                .setParentTypes(new ArrayList<>(
-                        Arrays.asList("Person", "Organization", "InterfaceRoot")))
-                .build();
-        assertThat(actualGenericDoc).isEqualTo(expectedGenericDoc);
+        List<SearchResult> searchResults = retrieveAllSearchResults(
+                mSession.search("", new SearchSpec.Builder().build())
+        );
+        assertThat(searchResults).hasSize(1);
+        SearchResult result = searchResults.get(0);
+        assertThat(result.getGenericDocument()).isEqualTo(genericDoc);
 
         // Deserializing it to InterfaceRoot will get a Person instance back, which is the first
         // known type, instead of an Organization.
-        InterfaceRoot interfaceRoot = actualGenericDoc.toDocumentClass(InterfaceRoot.class,
+        InterfaceRoot interfaceRoot = result.getDocument(InterfaceRoot.class,
                 AppSearchDocumentClassMap.getGlobalMap());
         assertThat(interfaceRoot).isInstanceOf(Person.class);
         assertThat(interfaceRoot).isNotInstanceOf(Organization.class);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
index b72cf54..addbaa3 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
@@ -21,12 +21,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
-import androidx.appsearch.testutil.AppSearchEmail;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -35,8 +35,6 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -110,315 +108,13 @@
         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
     }
 
+    // TODO(b/371610934): Remove this test once GenericDocument#setParentTypes is removed.
     @Test
-    public void testQuery_typeFilterWithPolymorphism() throws Exception {
+    @SuppressWarnings("deprecation")
+    public void testQuery_genericDocumentWrapsParentTypeForPolymorphism() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-
-        // Schema registration
-        AppSearchSchema personSchema =
-                new AppSearchSchema.Builder("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema artistSchema =
-                new AppSearchSchema.Builder("Artist")
-                        .addParentType("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(personSchema)
-                                .addSchemas(artistSchema)
-                                .addSchemas(AppSearchEmail.SCHEMA)
-                                .build())
-                .get();
-
-        // Index some documents
-        GenericDocument personDoc =
-                new GenericDocument.Builder<>("namespace", "id1", "Person")
-                        .setPropertyString("name", "Foo")
-                        .build();
-        GenericDocument artistDoc =
-                new GenericDocument.Builder<>("namespace", "id2", "Artist")
-                        .setPropertyString("name", "Foo")
-                        .build();
-        AppSearchEmail emailDoc =
-                new AppSearchEmail.Builder("namespace", "id3")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("Foo")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder()
-                                .addGenericDocuments(personDoc, artistDoc, emailDoc)
-                                .build()));
-        GenericDocument artistDocWithParent =
-                new GenericDocument.Builder<>(artistDoc).setParentTypes(
-                Collections.singletonList("Person")).build();
-
-        // Query for the documents
-        SearchResults searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(3);
-        assertThat(documents).containsExactly(personDoc, artistDocWithParent, emailDoc);
-
-        // Query with a filter for the "Person" type should also include the "Artist" type.
-        searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .addFilterSchemas("Person")
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(2);
-        assertThat(documents).containsExactly(personDoc, artistDocWithParent);
-
-        // Query with a filters for the "Artist" type should not include the "Person" type.
-        searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .addFilterSchemas("Artist")
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents).containsExactly(artistDocWithParent);
-    }
-
-    @Test
-    public void testQuery_projectionWithPolymorphism() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-
-        // Schema registration
-        AppSearchSchema personSchema =
-                new AppSearchSchema.Builder("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("emailAddress")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema artistSchema =
-                new AppSearchSchema.Builder("Artist")
-                        .addParentType("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("emailAddress")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("company")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(personSchema)
-                                .addSchemas(artistSchema)
-                                .build())
-                .get();
-
-        // Index two documents
-        GenericDocument personDoc =
-                new GenericDocument.Builder<>("namespace", "id1", "Person")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Person")
-                        .setPropertyString("emailAddress", "[email protected]")
-                        .build();
-        GenericDocument artistDoc =
-                new GenericDocument.Builder<>("namespace", "id2", "Artist")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Artist")
-                        .setPropertyString("emailAddress", "[email protected]")
-                        .setPropertyString("company", "Company")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder()
-                                .addGenericDocuments(personDoc, artistDoc)
-                                .build()));
-
-        // Query with type property paths {"Person", ["name"]}, {"Artist", ["emailAddress"]}
-        // This will be expanded to paths {"Person", ["name"]}, {"Artist", ["name", "emailAddress"]}
-        // via polymorphism.
-        SearchResults searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .addProjection("Person", ImmutableList.of("name"))
-                                .addProjection("Artist", ImmutableList.of("emailAddress"))
-                                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The person document should have been returned with only the "name" property. The artist
-        // document should have been returned with all of its properties.
-        GenericDocument expectedPerson =
-                new GenericDocument.Builder<>("namespace", "id1", "Person")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Person")
-                        .build();
-        GenericDocument expectedArtist =
-                new GenericDocument.Builder<>("namespace", "id2", "Artist")
-                        .setParentTypes(Collections.singletonList("Person"))
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Artist")
-                        .setPropertyString("emailAddress", "[email protected]")
-                        .build();
-        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
-    }
-
-    @Test
-    public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-
-        // Schema registration
-        AppSearchSchema personSchema =
-                new AppSearchSchema.Builder("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("emailAddress")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema artistSchema =
-                new AppSearchSchema.Builder("Artist")
-                        .addParentType("Person")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("name")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("emailAddress")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("company")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(personSchema)
-                                .addSchemas(artistSchema)
-                                .build())
-                .get();
-
-        // Index two documents
-        GenericDocument personDoc =
-                new GenericDocument.Builder<>("namespace", "id1", "Person")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Person")
-                        .setPropertyString("emailAddress", "[email protected]")
-                        .build();
-        GenericDocument artistDoc =
-                new GenericDocument.Builder<>("namespace", "id2", "Artist")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Artist")
-                        .setPropertyString("emailAddress", "[email protected]")
-                        .setPropertyString("company", "Company")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder()
-                                .addGenericDocuments(personDoc, artistDoc)
-                                .build()));
-
-        // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and
-        // a schema filter for the "Person".
-        // This will be expanded to paths {"Person", ["name"]} and
-        // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via
-        // polymorphism.
-        SearchResults searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .addFilterSchemas("Person")
-                                .addProjection("Person", ImmutableList.of("name"))
-                                .addProjection("Artist", ImmutableList.of("emailAddress"))
-                                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The person document should have been returned with only the "name" property. The artist
-        // document should have been returned with all of its properties.
-        GenericDocument expectedPerson =
-                new GenericDocument.Builder<>("namespace", "id1", "Person")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Person")
-                        .build();
-        GenericDocument expectedArtist =
-                new GenericDocument.Builder<>("namespace", "id2", "Artist")
-                        .setParentTypes(Collections.singletonList("Person"))
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("name", "Foo Artist")
-                        .setPropertyString("emailAddress", "[email protected]")
-                        .build();
-        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
-    }
-
-    @Test
-    public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        // When SearchResult does not wrap parent information, GenericDocument should do.
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
 
         // Schema registration
         AppSearchSchema personSchema =
@@ -449,12 +145,30 @@
                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                                         .build())
                         .build();
+        AppSearchSchema musicianSchema =
+                new AppSearchSchema.Builder("Musician")
+                        .addParentType("Artist")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
         AppSearchSchema messageSchema =
                 new AppSearchSchema.Builder("Message")
                         .addProperty(
                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                        "sender", "Person")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        "receivers", "Person")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
                                         .setShouldIndexNestedProperties(true)
                                         .build())
                         .build();
@@ -462,290 +176,59 @@
                         new SetSchemaRequest.Builder()
                                 .addSchemas(personSchema)
                                 .addSchemas(artistSchema)
+                                .addSchemas(musicianSchema)
                                 .addSchemas(messageSchema)
                                 .build())
                 .get();
 
-        // Index some an artistDoc and a messageDoc
+        // Index documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "person")
+                        .build();
         GenericDocument artistDoc =
-                new GenericDocument.Builder<>("namespace", "id1", "Artist")
-                        .setPropertyString("name", "Foo")
-                        .setPropertyString("company", "Bar")
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "artist")
+                        .setPropertyString("company", "foo")
+                        .build();
+        GenericDocument musicianDoc =
+                new GenericDocument.Builder<>("namespace", "id3", "Musician")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "musician")
+                        .setPropertyString("company", "foo")
                         .build();
         GenericDocument messageDoc =
-                new GenericDocument.Builder<>("namespace", "id2", "Message")
-                        // sender is defined as a Person, which accepts an Artist because Artist <:
-                        // Person.
-                        // However, indexing will be based on what's defined in Person, so the
-                        // "company"
-                        // property in artistDoc cannot be used to search this messageDoc.
-                        .setPropertyDocument("sender", artistDoc)
+                new GenericDocument.Builder<>("namespace", "id4", "Message")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyDocument("receivers", artistDoc, musicianDoc)
                         .build();
         checkIsBatchResultSuccess(
                 mDb1.putAsync(
                         new PutDocumentsRequest.Builder()
-                                .addGenericDocuments(artistDoc, messageDoc)
+                                .addGenericDocuments(personDoc, artistDoc, musicianDoc, messageDoc)
                                 .build()));
-        GenericDocument expectedArtistDoc =
+        GenericDocument artistDocWithParent =
                 new GenericDocument.Builder<>(artistDoc).setParentTypes(
-                Collections.singletonList("Person")).build();
-        GenericDocument expectedMessageDoc =
-                new GenericDocument.Builder<>(messageDoc).setPropertyDocument("sender",
-                expectedArtistDoc).build();
+                        Collections.singletonList("Person")).build();
+        GenericDocument musicianDocWithParent =
+                new GenericDocument.Builder<>(musicianDoc).setParentTypes(
+                        ImmutableList.of("Artist", "Person")).build();
+        GenericDocument messageDocWithParent =
+                new GenericDocument.Builder<>("namespace", "id4", "Message")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyDocument("receivers", artistDocWithParent,
+                                musicianDocWithParent)
+                        .build();
 
-        // Query for the documents
+        // Query to get all the documents
         SearchResults searchResults =
-                mDb1.search(
-                        "Foo",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
+                mDb1.search("", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(2);
-        assertThat(documents).containsExactly(expectedArtistDoc, expectedMessageDoc);
-
-        // The "company" property in artistDoc cannot be used to search messageDoc.
-        searchResults =
-                mDb1.search(
-                        "Bar",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents).containsExactly(expectedArtistDoc);
-    }
-
-    @Test
-    public void testQuery_parentTypeListIsTopologicalOrder() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-        // Create the following subtype relation graph, where
-        // 1. A's direct parents are B and C.
-        // 2. B's direct parent is D.
-        // 3. C's direct parent is B and D.
-        // DFS order from A: [A, B, D, C]. Not acceptable because B and D appear before C.
-        // BFS order from A: [A, B, C, D]. Not acceptable because B appears before C.
-        // Topological order (all subtypes appear before supertypes) from A: [A, C, B, D].
-        AppSearchSchema schemaA =
-                new AppSearchSchema.Builder("A")
-                        .addParentType("B")
-                        .addParentType("C")
-                        .build();
-        AppSearchSchema schemaB =
-                new AppSearchSchema.Builder("B")
-                        .addParentType("D")
-                        .build();
-        AppSearchSchema schemaC =
-                new AppSearchSchema.Builder("C")
-                        .addParentType("B")
-                        .addParentType("D")
-                        .build();
-        AppSearchSchema schemaD =
-                new AppSearchSchema.Builder("D")
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(schemaA)
-                                .addSchemas(schemaB)
-                                .addSchemas(schemaC)
-                                .addSchemas(schemaD)
-                                .build())
-                .get();
-
-        // Index some documents
-        GenericDocument docA =
-                new GenericDocument.Builder<>("namespace", "id1", "A")
-                        .build();
-        GenericDocument docB =
-                new GenericDocument.Builder<>("namespace", "id2", "B")
-                        .build();
-        GenericDocument docC =
-                new GenericDocument.Builder<>("namespace", "id3", "C")
-                        .build();
-        GenericDocument docD =
-                new GenericDocument.Builder<>("namespace", "id4", "D")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder()
-                                .addGenericDocuments(docA, docB, docC, docD)
-                                .build()));
-
-        GenericDocument expectedDocA = new GenericDocument.Builder<>(docA).setParentTypes(
-                        new ArrayList<>(Arrays.asList("C", "B", "D"))).build();
-        GenericDocument expectedDocB =
-                new GenericDocument.Builder<>(docB).setParentTypes(
-                        Collections.singletonList("D")).build();
-        GenericDocument expectedDocC =
-                new GenericDocument.Builder<>(docC).setParentTypes(
-                        new ArrayList<>(Arrays.asList("B", "D"))).build();
-        // Query for the documents
-        SearchResults searchResults = mDb1.search("", new SearchSpec.Builder().build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(4);
-        assertThat(documents).containsExactly(expectedDocA, expectedDocB, expectedDocC, docD);
-    }
-
-    // TODO(b/336277840): Move this if setParentTypes becomes public
-    @Test
-    public void testQuery_wildcardProjection_polymorphism() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-        AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
-                .addProperty(new StringPropertyConfig.Builder("sender")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("content")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-        AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
-                .addParentType("Message")
-                .addProperty(new StringPropertyConfig.Builder("sender")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("content")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
-                .addParentType("Message")
-                .addProperty(new StringPropertyConfig.Builder("sender")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("content")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-
-        // Schema registration
-        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
-                .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
-
-        // Index two child documents
-        GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
-                .setCreationTimestampMillis(1000)
-                .setPropertyString("sender", "Some sender")
-                .setPropertyString("content", "Some note")
-                .build();
-        GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
-                .setCreationTimestampMillis(1000)
-                .setPropertyString("sender", "Some sender")
-                .setPropertyString("content", "Some note")
-                .build();
-        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
-                .addGenericDocuments(email, text).build()));
-
-        SearchResults searchResults = mDb1.search("Some", new SearchSpec.Builder()
-                .addFilterSchemas("Message")
-                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))
-                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("content"))
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // We specified the parent document in the filter schemas, but only indexed child documents.
-        // As we also specified a wildcard schema type projection, it should apply to the child docs
-        // The content property must not appear. Also emailNoContent should not appear as we are
-        // filter on the content property
-        GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
-                .setParentTypes(Collections.singletonList("Message"))
-                .setCreationTimestampMillis(1000)
-                .setPropertyString("sender", "Some sender")
-                .build();
-        GenericDocument expectedEmail = new GenericDocument.Builder<>("namespace", "id2", "Email")
-                .setParentTypes(Collections.singletonList("Message"))
-                .setCreationTimestampMillis(1000)
-                .setPropertyString("sender", "Some sender")
-                .build();
-        assertThat(documents).containsExactly(expectedText, expectedEmail);
-    }
-
-    // TODO(b/336277840): Move this if setParentTypes becomes public
-    @Test
-    public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
-        AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
-                .addProperty(new StringPropertyConfig.Builder("content")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-        AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
-                .addParentType("Message")
-                .addProperty(new StringPropertyConfig.Builder("content")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("carrier")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
-                .addParentType("Message")
-                .addProperty(new StringPropertyConfig.Builder("content")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("attachment")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-
-        // Schema registration
-        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
-                .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
-
-        // Index two child documents
-        GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
-                .setCreationTimestampMillis(1000)
-                .setPropertyString("content", "Some note")
-                .setPropertyString("carrier", "Network Inc")
-                .build();
-        GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
-                .setCreationTimestampMillis(1000)
-                .setPropertyString("content", "Some note")
-                .setPropertyString("attachment", "Network report")
-                .build();
-
-        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
-                .addGenericDocuments(email, text).build()));
-
-        // Both email and text would match for "Network", but only text should match as it is in the
-        // right property
-        SearchResults searchResults = mDb1.search("Network", new SearchSpec.Builder()
-                .addFilterSchemas("Message")
-                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("carrier"))
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // We specified the parent document in the filter schemas, but only indexed child documents.
-        // As we also specified a wildcard schema type projection, it should apply to the child docs
-        // The content property must not appear. Also emailNoContent should not appear as we are
-        // filter on the content property
-        GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
-                .setParentTypes(Collections.singletonList("Message"))
-                .setCreationTimestampMillis(1000)
-                .setPropertyString("content", "Some note")
-                .setPropertyString("carrier", "Network Inc")
-                .build();
-        assertThat(documents).containsExactly(expectedText);
+        assertThat(documents).containsExactly(personDoc, artistDocWithParent, musicianDocWithParent,
+                messageDocWithParent);
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
index fc5be8f..5e0025e 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
@@ -70,6 +70,7 @@
     }
 
     @Test
+    @SuppressWarnings("deprecation")
     public void testRecreateFromParcelWithParentTypes() {
         GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
                 .setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
@@ -112,6 +113,7 @@
     }
 
     @Test
+    @SuppressWarnings("deprecation")
     public void testGenericDocumentBuilderDoesNotMutateOriginal() {
         GenericDocument oldDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
                 .setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
index bec84cb..443d726 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
@@ -20,6 +20,9 @@
 
 import org.junit.Test;
 
+import java.util.List;
+import java.util.Map;
+
 public class SearchResultInternalTest {
     @Test
     public void testSearchResultBuilderCopyConstructor() {
@@ -75,6 +78,22 @@
     }
 
     @Test
+    public void testSearchResultBuilderCopyConstructor_parentType() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "schemaType1").build();
+        SearchResult searchResult = new SearchResult.Builder("package", "database")
+                .setGenericDocument(document)
+                .setParentTypeMap(Map.of(
+                        "schemaType1", List.of("parent1", "parent2"),
+                        "schemaType2", List.of("parent3", "parent4")
+                ))
+                .build();
+        SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();
+        assertThat(searchResultCopy.getParentTypeMap()).containsExactlyEntriesIn(
+                searchResult.getParentTypeMap());
+    }
+
+    @Test
     public void testSearchResultBuilder_clearJoinedResults() {
         GenericDocument document =
                 new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index 3d87ab6..0b9308d 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -7863,6 +7863,141 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testQuery_searchResultWrapsParentTypeMapForPolymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema musicianSchema =
+                new AppSearchSchema.Builder("Musician")
+                        .addParentType("Artist")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema messageSchema =
+                new AppSearchSchema.Builder("Message")
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "receivers", "Person")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .addSchemas(musicianSchema)
+                                .addSchemas(messageSchema)
+                                .build())
+                .get();
+
+        // Index documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setPropertyString("name", "person")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setPropertyString("name", "artist")
+                        .setPropertyString("company", "foo")
+                        .build();
+        GenericDocument musicianDoc =
+                new GenericDocument.Builder<>("namespace", "id3", "Musician")
+                        .setPropertyString("name", "musician")
+                        .setPropertyString("company", "foo")
+                        .build();
+        GenericDocument messageDoc =
+                new GenericDocument.Builder<>("namespace", "id4", "Message")
+                        .setPropertyDocument("receivers", artistDoc, musicianDoc)
+                        .build();
+
+        Map<String, List<String>> expectedPersonParentTypeMap = Collections.emptyMap();
+        Map<String, List<String>> expectedArtistParentTypeMap =
+                ImmutableMap.of("Artist", ImmutableList.of("Person"));
+        Map<String, List<String>> expectedMusicianParentTypeMap =
+                ImmutableMap.of("Musician", ImmutableList.of("Artist", "Person"));
+        // artistDoc and musicianDoc are nested in messageDoc, so messageDoc's parent type map
+        // should have the entries for both the Artist and Musician type.
+        Map<String, List<String>> expectedMessageParentTypeMap = ImmutableMap.of(
+                "Artist", ImmutableList.of("Person"),
+                "Musician", ImmutableList.of("Artist", "Person"));
+
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc, musicianDoc, messageDoc)
+                                .build()));
+
+        // Query to get all the documents
+        List<SearchResult> searchResults = retrieveAllSearchResults(
+                mDb1.search("", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .build()));
+        assertThat(searchResults).hasSize(4);
+        assertThat(searchResults.get(0).getGenericDocument().getSchemaType())
+                .isEqualTo("Message");
+        assertThat(searchResults.get(0).getParentTypeMap())
+                .isEqualTo(expectedMessageParentTypeMap);
+
+        assertThat(searchResults.get(1).getGenericDocument().getSchemaType())
+                .isEqualTo("Musician");
+        assertThat(searchResults.get(1).getParentTypeMap())
+                .isEqualTo(expectedMusicianParentTypeMap);
+
+        assertThat(searchResults.get(2).getGenericDocument().getSchemaType())
+                .isEqualTo("Artist");
+        assertThat(searchResults.get(2).getParentTypeMap())
+                .isEqualTo(expectedArtistParentTypeMap);
+
+        assertThat(searchResults.get(3).getGenericDocument().getSchemaType())
+                .isEqualTo("Person");
+        assertThat(searchResults.get(3).getParentTypeMap())
+                .isEqualTo(expectedPersonParentTypeMap);
+    }
+
+    @Test
     public void testSimpleJoin() throws Exception {
         assumeTrue(mDb1.getFeatures()
                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
@@ -11283,6 +11418,655 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testQuery_typeFilterWithPolymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .build())
+                .get();
+
+        // Index some documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setPropertyString("name", "Foo")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setPropertyString("name", "Foo")
+                        .build();
+        AppSearchEmail emailDoc =
+                new AppSearchEmail.Builder("namespace", "id3")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("Foo")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc, emailDoc)
+                                .build()));
+
+        // Query for the documents
+        SearchResults searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(3);
+        assertThat(documents).containsExactly(personDoc, artistDoc, emailDoc);
+
+        // Query with a filter for the "Person" type should also include the "Artist" type.
+        searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Person")
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        assertThat(documents).containsExactly(personDoc, artistDoc);
+
+        // Query with a filters for the "Artist" type should not include the "Person" type.
+        searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Artist")
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(artistDoc);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testQuery_projectionWithPolymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .build())
+                .get();
+
+        // Index two documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .setPropertyString("company", "Company")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc)
+                                .build()));
+
+        // Query with type property paths {"Person", ["name"]}, {"Artist", ["emailAddress"]}
+        // This will be expanded to paths {"Person", ["name"]}, {"Artist", ["name", "emailAddress"]}
+        // via polymorphism.
+        SearchResults searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection("Person", ImmutableList.of("name"))
+                                .addProjection("Artist", ImmutableList.of("emailAddress"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The person document should have been returned with only the "name" property. The artist
+        // document should have been returned with all of its properties.
+        GenericDocument expectedPerson =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .build();
+        GenericDocument expectedArtist =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .build())
+                .get();
+
+        // Index two documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .setPropertyString("company", "Company")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc)
+                                .build()));
+
+        // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and
+        // a schema filter for the "Person".
+        // This will be expanded to paths {"Person", ["name"]} and
+        // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via
+        // polymorphism.
+        SearchResults searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addFilterSchemas("Person")
+                                .addProjection("Person", ImmutableList.of("name"))
+                                .addProjection("Artist", ImmutableList.of("emailAddress"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The person document should have been returned with only the "name" property. The artist
+        // document should have been returned with all of its properties.
+        GenericDocument expectedPerson =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .build();
+        GenericDocument expectedArtist =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema messageSchema =
+                new AppSearchSchema.Builder("Message")
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "sender", "Person")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .addSchemas(messageSchema)
+                                .build())
+                .get();
+
+        // Index some an artistDoc and a messageDoc
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Artist")
+                        .setPropertyString("name", "Foo")
+                        .setPropertyString("company", "Bar")
+                        .build();
+        GenericDocument messageDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Message")
+                        // sender is defined as a Person, which accepts an Artist because Artist <:
+                        // Person.
+                        // However, indexing will be based on what's defined in Person, so the
+                        // "company"
+                        // property in artistDoc cannot be used to search this messageDoc.
+                        .setPropertyDocument("sender", artistDoc)
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(artistDoc, messageDoc)
+                                .build()));
+
+        // Query for the documents
+        SearchResults searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        assertThat(documents).containsExactly(artistDoc, messageDoc);
+
+        // The "company" property in artistDoc cannot be used to search messageDoc.
+        searchResults =
+                mDb1.search(
+                        "Bar",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(artistDoc);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testQuery_parentTypeListIsTopologicalOrder() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
+        // Create the following subtype relation graph, where
+        // 1. A's direct parents are B and C.
+        // 2. B's direct parent is D.
+        // 3. C's direct parent is B and D.
+        // DFS order from A: [A, B, D, C]. Not acceptable because B and D appear before C.
+        // BFS order from A: [A, B, C, D]. Not acceptable because B appears before C.
+        // Topological order (all subtypes appear before supertypes) from A: [A, C, B, D].
+        AppSearchSchema schemaA =
+                new AppSearchSchema.Builder("A")
+                        .addParentType("B")
+                        .addParentType("C")
+                        .build();
+        AppSearchSchema schemaB =
+                new AppSearchSchema.Builder("B")
+                        .addParentType("D")
+                        .build();
+        AppSearchSchema schemaC =
+                new AppSearchSchema.Builder("C")
+                        .addParentType("B")
+                        .addParentType("D")
+                        .build();
+        AppSearchSchema schemaD =
+                new AppSearchSchema.Builder("D")
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(schemaA)
+                                .addSchemas(schemaB)
+                                .addSchemas(schemaC)
+                                .addSchemas(schemaD)
+                                .build())
+                .get();
+
+        // Index some documents
+        GenericDocument docA =
+                new GenericDocument.Builder<>("namespace", "id1", "A")
+                        .build();
+        GenericDocument docB =
+                new GenericDocument.Builder<>("namespace", "id2", "B")
+                        .build();
+        GenericDocument docC =
+                new GenericDocument.Builder<>("namespace", "id3", "C")
+                        .build();
+        GenericDocument docD =
+                new GenericDocument.Builder<>("namespace", "id4", "D")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(docA, docB, docC, docD)
+                                .build()));
+
+        Map<String, List<String>> expectedDocAParentTypeMap =
+                ImmutableMap.of("A", ImmutableList.of("C", "B", "D"));
+        Map<String, List<String>> expectedDocBParentTypeMap =
+                ImmutableMap.of("B", ImmutableList.of("D"));
+        Map<String, List<String>> expectedDocCParentTypeMap =
+                ImmutableMap.of("C", ImmutableList.of("B", "D"));
+        Map<String, List<String>> expectedDocDParentTypeMap = Collections.emptyMap();
+        // Query for the documents
+        List<SearchResult> searchResults = retrieveAllSearchResults(
+                mDb1.search("", new SearchSpec.Builder().build())
+        );
+        assertThat(searchResults).hasSize(4);
+        assertThat(searchResults.get(0).getGenericDocument()).isEqualTo(docD);
+        assertThat(searchResults.get(0).getParentTypeMap()).isEqualTo(expectedDocDParentTypeMap);
+
+        assertThat(searchResults.get(1).getGenericDocument()).isEqualTo(docC);
+        assertThat(searchResults.get(1).getParentTypeMap()).isEqualTo(expectedDocCParentTypeMap);
+
+        assertThat(searchResults.get(2).getGenericDocument()).isEqualTo(docB);
+        assertThat(searchResults.get(2).getParentTypeMap()).isEqualTo(expectedDocBParentTypeMap);
+
+        assertThat(searchResults.get(3).getGenericDocument()).isEqualTo(docA);
+        assertThat(searchResults.get(3).getParentTypeMap()).isEqualTo(expectedDocAParentTypeMap);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testQuery_wildcardProjection_polymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
+
+        AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
+                .addProperty(new StringPropertyConfig.Builder("sender")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
+                .addParentType("Message")
+                .addProperty(new StringPropertyConfig.Builder("sender")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addParentType("Message")
+                .addProperty(new StringPropertyConfig.Builder("sender")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
+
+        // Index two child documents
+        GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("sender", "Some sender")
+                .setPropertyString("content", "Some note")
+                .build();
+        GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("sender", "Some sender")
+                .setPropertyString("content", "Some note")
+                .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(email, text).build()));
+
+        SearchResults searchResults = mDb1.search("Some", new SearchSpec.Builder()
+                .addFilterSchemas("Message")
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("content"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // We specified the parent document in the filter schemas, but only indexed child documents.
+        // As we also specified a wildcard schema type projection, it should apply to the child docs
+        // The content property must not appear. Also emailNoContent should not appear as we are
+        // filter on the content property
+        GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("sender", "Some sender")
+                .build();
+        GenericDocument expectedEmail = new GenericDocument.Builder<>("namespace", "id2", "Email")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("sender", "Some sender")
+                .build();
+        assertThat(documents).containsExactly(expectedText, expectedEmail);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
+
+        AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
+                .addParentType("Message")
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("carrier")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addParentType("Message")
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("attachment")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
+
+        // Index two child documents
+        GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("content", "Some note")
+                .setPropertyString("carrier", "Network Inc")
+                .build();
+        GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("content", "Some note")
+                .setPropertyString("attachment", "Network report")
+                .build();
+
+        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(email, text).build()));
+
+        // Both email and text would match for "Network", but only text should match as it is in the
+        // right property
+        SearchResults searchResults = mDb1.search("Network", new SearchSpec.Builder()
+                .addFilterSchemas("Message")
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("carrier"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // We specified the parent document in the filter schemas, but only indexed child documents.
+        // As we also specified a wildcard schema type projection, it should apply to the child docs
+        // The content property must not appear. Also emailNoContent should not appear as we are
+        // filter on the content property
+        GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("content", "Some note")
+                .setPropertyString("carrier", "Network Inc")
+                .build();
+        assertThat(documents).containsExactly(expectedText);
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
     public void testRankWithNonScorableProperty() throws Exception {
         // TODO(b/379923400): Implement this test.
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
index 382d5272..3761d6f 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
@@ -31,6 +31,9 @@
 import org.junit.Rule;
 import org.junit.Test;
 
+import java.util.List;
+import java.util.Map;
+
 public class SearchResultCtsTest {
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@@ -223,4 +226,55 @@
         assertThat(rebuild.getInformationalRankingSignals())
                 .containsExactly(3.0, 4.0, 5.0).inOrder();
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testBuildSearchResult_parentTypeMap() {
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+                .setBody("Hello World.")
+                .build();
+        SearchResult searchResult = new SearchResult.Builder("packageName", "databaseName")
+                .setGenericDocument(email)
+                .setParentTypeMap(Map.of(
+                        "schema1", List.of("parent1", "parent2"),
+                        "schema2", List.of("parent3", "parent4")
+                ))
+                .build();
+
+        assertThat(searchResult.getParentTypeMap())
+                .containsExactly(
+                        "schema1", List.of("parent1", "parent2"),
+                        "schema2", List.of("parent3", "parent4")
+                ).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public void testRebuild_parentTypeMap() {
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+                .setBody("Hello World.")
+                .build();
+
+        SearchResult.Builder searchResultBuilder =
+                new SearchResult.Builder("packageName", "databaseName")
+                        .setGenericDocument(email)
+                        .setParentTypeMap(Map.of(
+                                "schema1", List.of("parent1", "parent2"),
+                                "schema2", List.of("parent3", "parent4")
+                        ));
+
+        SearchResult original = searchResultBuilder.build();
+        SearchResult rebuild = searchResultBuilder
+                .setParentTypeMap(Map.of("schema3", List.of("parent5", "parent6"))).build();
+
+        // Rebuild won't effect the original object
+        assertThat(original.getParentTypeMap())
+                .containsExactly(
+                        "schema1", List.of("parent1", "parent2"),
+                        "schema2", List.of("parent3", "parent4")
+                ).inOrder();
+
+        assertThat(rebuild.getParentTypeMap())
+                .containsExactly("schema3", List.of("parent5", "parent6")).inOrder();
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
index e0fd2a5..6c7023a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
@@ -177,4 +177,11 @@
         assertThat(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
                 .isEqualTo("com.android.appsearch.flags.enable_scorable_property");
     }
+
+    @Test
+    public void testFlagValue_enableSearchResultParentTypes() {
+        assertThat(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+                .isEqualTo(
+                        "com.android.appsearch.flags.enable_search_result_parent_types");
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
index 90033ee..2ae65ce 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
@@ -17,7 +17,6 @@
 package androidx.appsearch.app;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.appsearch.exceptions.AppSearchException;
 
 import java.util.List;
@@ -59,10 +58,19 @@
 
     /**
      * Converts a {@link androidx.appsearch.app.GenericDocument} into an instance of the document
-     * class. For nested document properties, this method should pass {@code documentClassMap} down
-     * to the nested calls of {@link GenericDocument#toDocumentClass(Class, Map)}.
+     * class. For nested document properties, this method should pass
+     * {@code documentClassMappingContext} down to the nested calls of
+     * {@link GenericDocument#toDocumentClass(Class, DocumentClassMappingContext)}.
+     *
+     * @param genericDoc                  The document to convert.
+     * @param documentClassMappingContext The context object that holds mapping information for
+     *                                    document classes and their parent types. This context
+     *                                    typically comes from
+     *                                    {@link SearchResult#getDocument(Class, Map)}.
      */
     @NonNull
+    @ExperimentalAppSearchApi
     T fromGenericDocument(@NonNull GenericDocument genericDoc,
-            @Nullable Map<String, List<String>> documentClassMap) throws AppSearchException;
+            @NonNull DocumentClassMappingContext documentClassMappingContext)
+            throws AppSearchException;
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassMappingContext.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassMappingContext.java
new file mode 100644
index 0000000..798e839
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassMappingContext.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.Document;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A context object that holds mapping information for document classes and their parent types.
+ *
+ * <p>This class encapsulates the {@code documentClassMap} and {@code parentTypeMap} used during
+ * the deserialization of {@link GenericDocument} instances into specific document classes.
+ *
+ * @see GenericDocument#toDocumentClass(Class, DocumentClassMappingContext)
+ */
+public class DocumentClassMappingContext {
+    /** An empty {@link DocumentClassMappingContext} instance. */
+    @ExperimentalAppSearchApi
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final DocumentClassMappingContext EMPTY =
+            new DocumentClassMappingContext(/* documentClassMap= */null, /* parentTypeMap= */null);
+
+    private final @NonNull Map<String, List<String>> mDocumentClassMap;
+    private final @NonNull Map<String, List<String>> mParentTypeMap;
+
+    /**
+     * Constructs a new {@link DocumentClassMappingContext}.
+     *
+     * @param documentClassMap A map from AppSearch's type name specified by {@link Document#name()}
+     *                         to the list of the fully qualified names of the corresponding
+     *                         document classes. In most cases, passing the value returned by
+     *                         {@link AppSearchDocumentClassMap#getGlobalMap()} will be sufficient.
+     * @param parentTypeMap    A map from AppSearch's type name specified by {@link Document#name()}
+     *                         to the list of its parent type names. In most cases, passing the
+     *                         value returned by {@link SearchResult#getParentTypeMap()} will be
+     *                         sufficient.
+     */
+    @ExperimentalAppSearchApi
+    public DocumentClassMappingContext(
+            @Nullable Map<String, List<String>> documentClassMap,
+            @Nullable Map<String, List<String>> parentTypeMap) {
+        mDocumentClassMap = documentClassMap != null ? Collections.unmodifiableMap(documentClassMap)
+                : Collections.emptyMap();
+        mParentTypeMap = parentTypeMap != null ? Collections.unmodifiableMap(parentTypeMap)
+                : Collections.emptyMap();
+    }
+
+    /**
+     * Returns the document class map.
+     */
+    @NonNull
+    @ExperimentalAppSearchApi
+    public Map<String, List<String>> getDocumentClassMap() {
+        return mDocumentClassMap;
+    }
+
+    /**
+     * Returns the parent type map.
+     */
+    @NonNull
+    @ExperimentalAppSearchApi
+    public Map<String, List<String>> getParentTypeMap() {
+        return mParentTypeMap;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index 9f43cc1..0777ac6 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -280,6 +280,15 @@
     String BLOB_STORAGE = "BLOB_STORAGE";
 
     /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers whether to wrap the
+     * parent types of a document in the corresponding
+     * {@link androidx.appsearch.app.SearchResult}, instead of in
+     * {@link androidx.appsearch.app.GenericDocument}.
+     */
+    @ExperimentalAppSearchApi
+    String SEARCH_RESULT_PARENT_TYPES = "SEARCH_RESULT_PARENT_TYPES";
+
+    /**
      * Returns whether a feature is supported at run-time. Feature support depends on the
      * feature in question, the AppSearch backend being used and the Android version of the
      * device.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index d99dc3b..04e5586 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -257,9 +257,13 @@
      * Returns the list of parent types of the {@link GenericDocument}'s type.
      *
      * <p>It is guaranteed that child types appear before parent types in the list.
+     *
+     * @deprecated Parent types should no longer be set in {@link GenericDocument}. Use
+     * {@link SearchResult.Builder#getParentTypeMap()} instead.
      * <!--@exportToFramework:hide-->
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Deprecated
     @Nullable
     public List<String> getParentTypes() {
         List<String> result = mDocumentParcel.getParentTypes();
@@ -1089,8 +1093,9 @@
      * @see GenericDocument#fromDocumentClass
      */
     @NonNull
+    @OptIn(markerClass = ExperimentalAppSearchApi.class)
     public <T> T toDocumentClass(@NonNull Class<T> documentClass) throws AppSearchException {
-        return toDocumentClass(documentClass, /* documentClassMap= */null);
+        return toDocumentClass(documentClass, DocumentClassMappingContext.EMPTY);
     }
 
     /**
@@ -1106,11 +1111,12 @@
      *
      * <p>If this GenericDocument's type is recorded as a subtype of the provided
      * {@code documentClass}, the method will find an AppSearch document class, using the provided
-     * {@code documentClassMap}, that is the most concrete and assignable to {@code documentClass},
-     * and then deserialize to that class instead. This allows for more specific and accurate
-     * deserialization of GenericDocuments. If {@code documentClassMap} is null or we are not
-     * able to find a candidate assignable to {@code documentClass}, the method will deserialize
-     * to {@code documentClass} directly.
+     * {@code documentClassMappingContext}, that is the most concrete and assignable to
+     * {@code documentClass}, and then deserialize to that class instead. This allows for more
+     * specific and accurate deserialization of GenericDocuments. If
+     * {@code documentClassMappingContext} has information missing or we are not able to find a
+     * candidate assignable to {@code documentClass}, the method will deserialize to
+     * {@code documentClass} directly.
      *
      * <p>Assignability is determined by the programing language's type system, and which type is
      * more concrete is determined by AppSearch's type system specified via
@@ -1118,13 +1124,15 @@
      * {@link Document#parent()}.
      *
      * <p>For nested document properties, this method will be called recursively, and
-     * {@code documentClassMap} will be passed down to the recursive calls of this method.
+     * {@code documentClassMappingContext} will be passed down to the recursive
+     * calls of this method.
      *
-     * @param documentClass    a class annotated with {@link Document}
-     * @param documentClassMap a map from AppSearch's type name specified by {@link Document#name()}
-     *                         to the list of the fully qualified names of the corresponding
-     *                         document classes. In most cases, passing the value returned by
-     *                         {@link AppSearchDocumentClassMap#getGlobalMap()} will be sufficient.
+     * <p>For most use cases, it is recommended to utilize
+     * {@link SearchResult#getDocument(Class, Map)} instead of calling this method directly. This
+     * avoids the need to manually create a {@link DocumentClassMappingContext}.
+     *
+     * @param documentClass               a class annotated with {@link Document}
+     * @param documentClassMappingContext a {@link DocumentClassMappingContext} instance
      * @return an instance of the document class after being converted from a
      * {@link GenericDocument}
      * @throws AppSearchException if no factory for this document class could be found on the
@@ -1132,20 +1140,26 @@
      * @see GenericDocument#fromDocumentClass
      */
     @NonNull
+    @ExperimentalAppSearchApi
     public <T> T toDocumentClass(@NonNull Class<T> documentClass,
-            @Nullable Map<String, List<String>> documentClassMap) throws AppSearchException {
+            @NonNull DocumentClassMappingContext documentClassMappingContext)
+            throws AppSearchException {
         Preconditions.checkNotNull(documentClass);
+        Preconditions.checkNotNull(documentClassMappingContext);
         DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
         Class<? extends T> targetClass = findTargetClassToDeserialize(documentClass,
-                documentClassMap);
+                documentClassMappingContext.getDocumentClassMap(),
+                documentClassMappingContext.getParentTypeMap());
         DocumentClassFactory<? extends T> factory = registry.getOrCreateFactory(targetClass);
-        return factory.fromGenericDocument(this, documentClassMap);
+        return factory.fromGenericDocument(this, documentClassMappingContext);
     }
 
     /**
      * Find a target class that is assignable to {@code documentClass} to deserialize this
-     * document, based on the provided document class map. If the provided map is null, return
-     * {@code documentClass} directly.
+     * document, based on the provided {@code documentClassMap} and {@code parentTypeMap}. If
+     * {@code documentClassMap} is empty, return {@code documentClass} directly.
+     * If {@code parentTypeMap} does not contain the required parent type information, this
+     * method will try the deprecated {@link #getParentTypes()}.
      *
      * <p>This method first tries to find a target class corresponding to the document's own type.
      * If that fails, it then tries to find a class corresponding to the document's parent type.
@@ -1153,8 +1167,9 @@
      */
     @NonNull
     private <T> Class<? extends T> findTargetClassToDeserialize(@NonNull Class<T> documentClass,
-            @Nullable Map<String, List<String>> documentClassMap) {
-        if (documentClassMap == null) {
+            @NonNull Map<String, List<String>> documentClassMap,
+            @NonNull Map<String, List<String>> parentTypeMap) {
+        if (documentClassMap.isEmpty()) {
             return documentClass;
         }
 
@@ -1166,7 +1181,12 @@
         }
 
         // Find the target class by parent types.
-        List<String> parentTypes = getParentTypes();
+        List<String> parentTypes;
+        if (parentTypeMap.containsKey(getSchemaType())) {
+            parentTypes = parentTypeMap.get(getSchemaType());
+        } else {
+            parentTypes = getParentTypes();
+        }
         if (parentTypes != null) {
             for (int i = 0; i < parentTypes.size(); ++i) {
                 targetClass = AppSearchDocumentClassMap.getAssignableClassBySchemaName(
@@ -1422,13 +1442,16 @@
          * Sets the list of parent types of the {@link GenericDocument}'s type.
          *
          * <p>Child types must appear before parent types in the list.
+         *
+         * @deprecated Parent types should no longer be set in {@link GenericDocument}. Use
+         * {@link SearchResult.Builder#setParentTypeMap(Map)} instead.
          * <!--@exportToFramework:hide-->
          */
         @CanIgnoreReturnValue
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
         @NonNull
-        public BuilderType setParentTypes(@NonNull List<String> parentTypes) {
-            Preconditions.checkNotNull(parentTypes);
+        public BuilderType setParentTypes(@Nullable List<String> parentTypes) {
             mDocumentParcelBuilder.setParentTypes(parentTypes);
             return mBuilderTypeInstance;
         }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
index efa6248..736b84e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.app;
 
+import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
@@ -32,6 +34,8 @@
 import androidx.appsearch.safeparcel.SafeParcelable;
 import androidx.appsearch.safeparcel.stub.StubCreators.MatchInfoCreator;
 import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultCreator;
+import androidx.appsearch.util.BundleUtil;
+import androidx.collection.ArrayMap;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
@@ -39,6 +43,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * This class represents one of the results obtained from an AppSearch query.
@@ -78,6 +83,20 @@
     @NonNull
     @Field(id = 7, getter = "getInformationalRankingSignals")
     private final List<Double> mInformationalRankingSignals;
+    /**
+     * Holds the map from schema type names to the list of their parent types.
+     *
+     * <p>The map includes entries for the {@link GenericDocument}'s own type and all of the
+     * nested documents' types. Child types are guaranteed to appear before parent types in each
+     * list.
+     *
+     * <p>Parent types include transitive parents.
+     *
+     * <p>All schema names in this map are un-prefixed, for both keys and values.
+     */
+    @NonNull
+    @Field(id = 8, getter = "getParentTypeMap")
+    private final Bundle mParentTypeMap;
 
 
     /** Cache of the {@link GenericDocument}. Comes from mDocument at first use. */
@@ -97,7 +116,8 @@
             @Param(id = 4) @NonNull String databaseName,
             @Param(id = 5) double rankingSignal,
             @Param(id = 6) @NonNull List<SearchResult> joinedResults,
-            @Param(id = 7) @Nullable List<Double> informationalRankingSignals) {
+            @Param(id = 7) @Nullable List<Double> informationalRankingSignals,
+            @Param(id = 8) @Nullable Bundle parentTypeMap) {
         mDocument = Preconditions.checkNotNull(document);
         mMatchInfos = Preconditions.checkNotNull(matchInfos);
         mPackageName = Preconditions.checkNotNull(packageName);
@@ -110,6 +130,11 @@
         } else {
             mInformationalRankingSignals = Collections.emptyList();
         }
+        if (parentTypeMap != null) {
+            mParentTypeMap = parentTypeMap;
+        } else {
+            mParentTypeMap = Bundle.EMPTY;
+        }
     }
 
 // @exportToFramework:startStrip()
@@ -118,12 +143,15 @@
      *
      * <p>This is equivalent to calling {@code getGenericDocument().toDocumentClass(T.class)}.
      *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the object returned
+     * by this function, rather than calling it multiple times.
+     *
      * @param documentClass the document class to be passed to
-     *                      {@link GenericDocument#toDocumentClass}.
+     *                      {@link GenericDocument#toDocumentClass(java.lang.Class)}.
      * @return Document object which matched the query.
      * @throws AppSearchException if no factory for this document class could be found on the
      *       classpath.
-     * @see GenericDocument#toDocumentClass
+     * @see GenericDocument#toDocumentClass(java.lang.Class)
      */
     @NonNull
     public <T> T getDocument(@NonNull java.lang.Class<T> documentClass) throws AppSearchException {
@@ -134,22 +162,31 @@
      * Contains the matching document, converted to the given document class.
      *
      * <p>This is equivalent to calling {@code getGenericDocument().toDocumentClass(T.class,
-     * documentClassMap)}.
+     * new DocumentClassMappingContext(documentClassMap, getParentTypeMap()))}.
+     *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the object returned
+     * by this function, rather than calling it multiple times.
      *
      * @param documentClass the document class to be passed to
-     *                      {@link GenericDocument#toDocumentClass}.
-     * @param documentClassMap the document class map to be passed to
-     *                         {@link GenericDocument#toDocumentClass}.
+     *        {@link GenericDocument#toDocumentClass(java.lang.Class, DocumentClassMappingContext)}.
+     * @param documentClassMap A map from AppSearch's type name specified by
+     *                         {@link androidx.appsearch.annotation.Document#name()}
+     *                         to the list of the fully qualified names of the corresponding
+     *                         document classes. In most cases, passing the value returned by
+     *                         {@link AppSearchDocumentClassMap#getGlobalMap()} will be sufficient.
      * @return Document object which matched the query.
      * @throws AppSearchException if no factory for this document class could be found on the
      *                            classpath.
-     * @see GenericDocument#toDocumentClass
+     * @see GenericDocument#toDocumentClass(java.lang.Class, DocumentClassMappingContext)
      */
     @NonNull
+    @OptIn(markerClass = ExperimentalAppSearchApi.class)
     public <T> T getDocument(@NonNull java.lang.Class<T> documentClass,
             @Nullable Map<String, List<String>> documentClassMap) throws AppSearchException {
         Preconditions.checkNotNull(documentClass);
-        return getGenericDocument().toDocumentClass(documentClass, documentClassMap);
+        return getGenericDocument().toDocumentClass(documentClass,
+                new DocumentClassMappingContext(documentClassMap,
+                        getParentTypeMap()));
     }
 // @exportToFramework:endStrip()
 
@@ -253,6 +290,33 @@
     }
 
     /**
+     * Returns the map from schema type names to the list of their parent types.
+     *
+     * <p>The map includes entries for the {@link GenericDocument}'s own type and all of the
+     * nested documents' types. Child types are guaranteed to appear before parent types in each
+     * list.
+     *
+     * <p>Parent types include transitive parents.
+     *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
+     * by this function, rather than calling it multiple times.
+     */
+    @NonNull
+    @ExperimentalAppSearchApi
+    @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+    public Map<String, List<String>> getParentTypeMap() {
+        Set<String> schemaTypes = mParentTypeMap.keySet();
+        Map<String, List<String>> parentTypeMap = new ArrayMap<>(schemaTypes.size());
+        for (String schemaType : schemaTypes) {
+            ArrayList<String> parentTypes = mParentTypeMap.getStringArrayList(schemaType);
+            if (parentTypes != null) {
+                parentTypeMap.put(schemaType, parentTypes);
+            }
+        }
+        return parentTypeMap;
+    }
+
+    /**
      * Gets a list of {@link SearchResult} joined from the join operation.
      *
      * <p> These joined documents match the outer document as specified in the {@link JoinSpec}
@@ -286,6 +350,7 @@
         private GenericDocument mGenericDocument;
         private double mRankingSignal;
         private List<Double> mInformationalRankingSignals = new ArrayList<>();
+        private Bundle mParentTypeMap = new Bundle();
         private List<SearchResult> mJoinedResults = new ArrayList<>();
         private boolean mBuilt = false;
 
@@ -302,6 +367,7 @@
 
         /** @exportToFramework:hide */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @OptIn(markerClass = ExperimentalAppSearchApi.class)
         public Builder(@NonNull SearchResult searchResult) {
             Preconditions.checkNotNull(searchResult);
             mPackageName = searchResult.getPackageName();
@@ -310,6 +376,7 @@
             mRankingSignal = searchResult.getRankingSignal();
             mInformationalRankingSignals = new ArrayList<>(
                     searchResult.getInformationalRankingSignals());
+            setParentTypeMap(searchResult.getParentTypeMap());
             List<MatchInfo> matchInfos = searchResult.getMatchInfos();
             for (int i = 0; i < matchInfos.size(); i++) {
                 addMatchInfo(new MatchInfo.Builder(matchInfos.get(i)).build());
@@ -381,6 +448,46 @@
             return this;
         }
 
+        /**
+         * Sets the map from schema type names to the list of their parent types.
+         *
+         * <p>The map should include entries for the {@link GenericDocument}'s own type and all
+         * of the nested documents' types.
+         *
+         * <p>Child types must appear before parent types in each list. Otherwise, the
+         *  <!--@exportToFramework:ifJetpack()-->
+         *  {@link GenericDocument#toDocumentClass(java.lang.Class, DocumentClassMappingContext)}
+         *  <!--@exportToFramework:else()
+         *  GenericDocument's toDocumentClass
+         *  -->
+         * method may not correctly identify the most concrete type. This could lead to unintended
+         * deserialization into a more general type instead of a more specific type.
+         *
+         * <p>Parent types should include transitive parents.
+         */
+        @CanIgnoreReturnValue
+        @ExperimentalAppSearchApi
+        @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
+        @NonNull
+        public Builder setParentTypeMap(@NonNull Map<String, List<String>> parentTypeMap) {
+            Preconditions.checkNotNull(parentTypeMap);
+            resetIfBuilt();
+            mParentTypeMap.clear();
+
+            for (Map.Entry<String, List<String>> entry : parentTypeMap.entrySet()) {
+                Preconditions.checkNotNull(entry.getKey());
+                Preconditions.checkNotNull(entry.getValue());
+
+                ArrayList<String> parentTypes = new ArrayList<>(entry.getValue().size());
+                for (int i = 0; i < entry.getValue().size(); i++) {
+                    String parentType = entry.getValue().get(i);
+                    parentTypes.add(Preconditions.checkNotNull(parentType));
+                }
+                mParentTypeMap.putStringArrayList(entry.getKey(), parentTypes);
+            }
+            return this;
+        }
+
 
         /**
          * Adds a {@link SearchResult} that was joined by the {@link JoinSpec}.
@@ -419,7 +526,8 @@
                     mDatabaseName,
                     mRankingSignal,
                     mJoinedResults,
-                    mInformationalRankingSignals);
+                    mInformationalRankingSignals,
+                    mParentTypeMap);
         }
 
         private void resetIfBuilt() {
@@ -427,6 +535,7 @@
                 mMatchInfos = new ArrayList<>(mMatchInfos);
                 mJoinedResults = new ArrayList<>(mJoinedResults);
                 mInformationalRankingSignals = new ArrayList<>(mInformationalRankingSignals);
+                mParentTypeMap = BundleUtil.deepCopy(mParentTypeMap);
                 mBuilt = false;
             }
         }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
index 9b77160..8df00e8 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
@@ -182,6 +182,14 @@
     public static final String FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS =
             FLAG_PREFIX + "enable_additional_builder_copy_constructors";
 
+    /**
+     * Enables wrapping the parent types of a document in the corresponding
+     * {@link androidx.appsearch.app.SearchResult}, instead of in
+     * {@link androidx.appsearch.app.GenericDocument}.
+     */
+    public static final String FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES =
+            FLAG_PREFIX + "enable_search_result_parent_types";
+
     // Whether the features should be enabled.
     //
     // In Jetpack, those should always return true.
@@ -357,4 +365,13 @@
     public static boolean enableScorableProperty() {
         return true;
     }
+
+    /**
+     * Whether to wrap the parent types of a document in the corresponding
+     * {@link androidx.appsearch.app.SearchResult}, instead of in
+     * {@link androidx.appsearch.app.GenericDocument}.
+     */
+    public static boolean enableSearchResultParentTypes() {
+        return true;
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
index bc61a06..770ee0b 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
@@ -434,9 +434,12 @@
          */
         @CanIgnoreReturnValue
         @NonNull
-        public Builder setParentTypes(@NonNull List<String> parentTypes) {
-            Objects.requireNonNull(parentTypes);
-            mParentTypes = new ArrayList<>(parentTypes);
+        public Builder setParentTypes(@Nullable List<String> parentTypes) {
+            if (parentTypes == null) {
+                mParentTypes = null;
+            } else {
+                mParentTypes = new ArrayList<>(parentTypes);
+            }
             return this;
         }
 
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
index 7296edd..040cd42 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
@@ -18,6 +18,7 @@
 
 import static androidx.appsearch.compiler.CodegenUtils.createNewArrayExpr;
 import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_EXCEPTION_CLASS;
+import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_CLASS_MAPPING_CONTEXT_CLASS;
 import static androidx.appsearch.compiler.IntrospectionHelper.GENERIC_DOCUMENT_CLASS;
 
 import androidx.annotation.NonNull;
@@ -29,10 +30,8 @@
 import androidx.appsearch.compiler.annotationwrapper.SerializerClass;
 import androidx.appsearch.compiler.annotationwrapper.StringPropertyAnnotation;
 
-import com.squareup.javapoet.ClassName;
 import com.squareup.javapoet.CodeBlock;
 import com.squareup.javapoet.MethodSpec;
-import com.squareup.javapoet.ParameterizedTypeName;
 import com.squareup.javapoet.TypeName;
 import com.squareup.javapoet.TypeSpec;
 
@@ -76,16 +75,12 @@
     private MethodSpec createFromGenericDocumentMethod() {
         // Method header
         TypeName documentClass = TypeName.get(mModel.getClassElement().asType());
-        // The type of documentClassMap is Map<String, List<String>>.
-        TypeName documentClassMapType = ParameterizedTypeName.get(ClassName.get(Map.class),
-                ClassName.get(String.class),
-                ParameterizedTypeName.get(ClassName.get(List.class), ClassName.get(String.class)));
         MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("fromGenericDocument")
                 .addModifiers(Modifier.PUBLIC)
                 .returns(documentClass)
                 .addAnnotation(Override.class)
                 .addParameter(GENERIC_DOCUMENT_CLASS, "genericDoc")
-                .addParameter(documentClassMapType, "documentClassMap")
+                .addParameter(DOCUMENT_CLASS_MAPPING_CONTEXT_CLASS, "documentClassMappingContext")
                 .addException(APPSEARCH_EXCEPTION_CLASS);
 
         // Unpack properties from the GenericDocument into the format desired by the document class.
@@ -489,7 +484,9 @@
                         getterOrField.getJvmName(), ArrayList.class, getterOrField.getJvmName())
                 .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)",
                         getterOrField.getJvmName())
-                .addStatement("$NConv.add($NCopy[i].toDocumentClass($T.class, documentClassMap))",
+                .addStatement(
+                        "$NConv.add($NCopy[i].toDocumentClass($T.class, "
+                                + "documentClassMappingContext))",
                         getterOrField.getJvmName(),
                         getterOrField.getJvmName(),
                         getterOrField.getComponentType())
@@ -611,7 +608,9 @@
                         getterOrField.getJvmName())
                 .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)",
                         getterOrField.getJvmName())
-                .addStatement("$NConv[i] = $NCopy[i].toDocumentClass($T.class, documentClassMap)",
+                .addStatement(
+                        "$NConv[i] = $NCopy[i].toDocumentClass($T.class, "
+                                + "documentClassMappingContext)",
                         getterOrField.getJvmName(),
                         getterOrField.getJvmName(),
                         getterOrField.getComponentType())
@@ -728,7 +727,8 @@
                 .addStatement("$T $NConv = null",
                         getterOrField.getJvmType(), getterOrField.getJvmName())
                 .beginControlFlow("if ($NCopy != null)", getterOrField.getJvmName())
-                .addStatement("$NConv = $NCopy.toDocumentClass($T.class, documentClassMap)",
+                .addStatement(
+                        "$NConv = $NCopy.toDocumentClass($T.class, documentClassMappingContext)",
                         getterOrField.getJvmName(),
                         getterOrField.getJvmName(),
                         getterOrField.getJvmType())
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index dba768a..5a7bd2e 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -105,6 +105,9 @@
     static final ClassName RESTRICT_TO_SCOPE_CLASS =
             RESTRICT_TO_ANNOTATION_CLASS.nestedClass("Scope");
 
+    static final ClassName DOCUMENT_CLASS_MAPPING_CONTEXT_CLASS =
+            ClassName.get(APPSEARCH_PKG, "DocumentClassMappingContext");
+
     public final TypeMirror mStringType;
     public final TypeMirror mLongPrimitiveType;
     public final TypeMirror mIntPrimitiveType;
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index fa7b7aa..efa452b 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -2335,7 +2335,7 @@
         checkResultContains("Thing.java",
                 "Thing document = Thing.create(getIdConv, getNamespaceConv)");
         checkResultContains("Gift.java",
-                "thingConv = thingCopy.toDocumentClass(Thing.class, documentClassMap)");
+                "thingConv = thingCopy.toDocumentClass(Thing.class, documentClassMappingContext)");
         checkEqualsGolden("Gift.java");
     }
 
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
index 387d97d..089357c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -15,7 +16,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -109,7 +109,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] stringPropCopy = genericDoc.getPropertyStringArray("stringProp");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
index b9053a0..339def6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -49,7 +49,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     long creationTsConv = genericDoc.getCreationTimestampMillis();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
index 66843ec..44f7683 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -49,7 +49,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int scoreConv = genericDoc.getScore();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnClassGetter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnClassGetter.JAVA
index 89a4f42..349343c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnClassGetter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnClassGetter.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnClassGetterUsingFactory.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnClassGetterUsingFactory.JAVA
index 547faff..2470eba 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnClassGetterUsingFactory.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnClassGetterUsingFactory.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String getNamespaceConv = genericDoc.getNamespace();
     String getIdConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnInterfaceGetter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnInterfaceGetter.JAVA
index a3ba288..a167313 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnInterfaceGetter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAnnotationOnInterfaceGetter.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String getNamespaceConv = genericDoc.getNamespace();
     String getIdConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA
index 9a0e4a1..b1262ef 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -51,7 +51,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] propertyCopy = genericDoc.getPropertyStringArray("property");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocument.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocument.JAVA
index 8fc7620..a6e7c58 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocument.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocument.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -51,7 +51,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String idConv = genericDoc.getId();
     String namespaceConv = genericDoc.getNamespace();
     String[] propertyCopy = genericDoc.getPropertyStringArray("property");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
index 6317b89..df85788 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -11,7 +12,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -79,7 +79,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] repeatReqCopy = genericDoc.getPropertyStringArray("repeatReq");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testClassSpecialValues.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testClassSpecialValues.JAVA
index 9e24b3b..fe7927d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testClassSpecialValues.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testClassSpecialValues.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -11,7 +12,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -65,7 +65,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String mNamespaceConv = genericDoc.getNamespace();
     String mIdConv = genericDoc.getId();
     Long mCreationTimestampMillisConv = genericDoc.getCreationTimestampMillis();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilder.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilder.JAVA
index 458a025..26a5b6f 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilder.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilder.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String getNamespaceConv = genericDoc.getNamespace();
     String getIdConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderAnnotatingBuilderClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderAnnotatingBuilderClass.JAVA
index 66798f3..fb5e82f 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderAnnotatingBuilderClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderAnnotatingBuilderClass.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String getNamespaceConv = genericDoc.getNamespace();
     String getIdConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderOnly.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderOnly.JAVA
index 06074a4..d11873b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderOnly.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderOnly.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int priceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithAutoValue.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithAutoValue.JAVA
index 28fec1e..d38e8af 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithAutoValue.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithAutoValue.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String idConv = genericDoc.getId();
     String namespaceConv = genericDoc.getNamespace();
     int priceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithParameter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithParameter.JAVA
index 4fa4c99..ebdef46 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithParameter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithParameter.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String getNamespaceConv = genericDoc.getNamespace();
     String getIdConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithParameterAnnotatingBuilderClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithParameterAnnotatingBuilderClass.JAVA
index 0d82055..c09d525 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithParameterAnnotatingBuilderClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCreationByBuilderWithParameterAnnotatingBuilderClass.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String getNamespaceConv = genericDoc.getNamespace();
     String getIdConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
index 5dbc8f1..3dec9cb 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -41,7 +41,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     Gift document = new Gift();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
index 9447804..4866a73 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -12,7 +13,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -104,7 +104,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGeneratedCodeRestrictedToLibrary.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGeneratedCodeRestrictedToLibrary.JAVA
index 242946f..a499056 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGeneratedCodeRestrictedToLibrary.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGeneratedCodeRestrictedToLibrary.JAVA
@@ -3,6 +3,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -10,7 +11,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -43,7 +43,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     Gift document = new Gift();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA
index db8540b..2de5161 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int priceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListEmpty.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListEmpty.JAVA
index 553f36b..43c1cc7 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListEmpty.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListEmpty.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -62,7 +62,7 @@
 
   @Override
   public Person fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
@@ -73,7 +73,7 @@
     GenericDocument livesAtCopy = genericDoc.getPropertyDocument("livesAt");
     Address livesAtConv = null;
     if (livesAtCopy != null) {
-      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMap);
+      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMappingContext);
     }
     Person document = new Person();
     document.namespace = namespaceConv;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListImplicitInheritance.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListImplicitInheritance.JAVA
index ac8da6b..0838213 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListImplicitInheritance.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListImplicitInheritance.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -76,7 +76,7 @@
 
   @Override
   public Artist fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
@@ -87,7 +87,7 @@
     GenericDocument livesAtCopy = genericDoc.getPropertyDocument("livesAt");
     Address livesAtConv = null;
     if (livesAtCopy != null) {
-      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMap);
+      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMappingContext);
     }
     String[] mostFamousWorkCopy = genericDoc.getPropertyStringArray("mostFamousWork");
     String mostFamousWorkConv = null;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListImplicitlyInheritFromMultipleLevels.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListImplicitlyInheritFromMultipleLevels.JAVA
index ce3287d..09581df 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListImplicitlyInheritFromMultipleLevels.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListImplicitlyInheritFromMultipleLevels.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -86,7 +86,7 @@
 
   @Override
   public ArtistEmployee fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
@@ -97,7 +97,7 @@
     GenericDocument livesAtCopy = genericDoc.getPropertyDocument("livesAt");
     Address livesAtConv = null;
     if (livesAtCopy != null) {
-      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMap);
+      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMappingContext);
     }
     String[] mostFamousWorkCopy = genericDoc.getPropertyStringArray("mostFamousWork");
     String mostFamousWorkConv = null;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritSuperclassFalse.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritSuperclassFalse.JAVA
index e4d5805..17044ef 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritSuperclassFalse.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritSuperclassFalse.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -75,7 +75,7 @@
 
   @Override
   public Artist fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
@@ -86,7 +86,7 @@
     GenericDocument livesAtCopy = genericDoc.getPropertyDocument("livesAt");
     Address livesAtConv = null;
     if (livesAtCopy != null) {
-      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMap);
+      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMappingContext);
     }
     String[] mostFamousWorkCopy = genericDoc.getPropertyStringArray("mostFamousWork");
     String mostFamousWorkConv = null;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritSuperclassTrue.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritSuperclassTrue.JAVA
index 1620454..3dd6c48 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritSuperclassTrue.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritSuperclassTrue.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -77,7 +77,7 @@
 
   @Override
   public Artist fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
@@ -88,7 +88,7 @@
     GenericDocument livesAtCopy = genericDoc.getPropertyDocument("livesAt");
     Address livesAtConv = null;
     if (livesAtCopy != null) {
-      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMap);
+      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMappingContext);
     }
     String[] mostFamousWorkCopy = genericDoc.getPropertyStringArray("mostFamousWork");
     String mostFamousWorkConv = null;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritWithMultipleParentsClasses.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritWithMultipleParentsClasses.JAVA
index 3c4c645..d560555 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritWithMultipleParentsClasses.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListInheritWithMultipleParentsClasses.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -79,7 +79,7 @@
 
   @Override
   public ArtistEmployee fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
@@ -90,7 +90,7 @@
     GenericDocument livesAtCopy = genericDoc.getPropertyDocument("livesAt");
     Address livesAtConv = null;
     if (livesAtCopy != null) {
-      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMap);
+      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMappingContext);
     }
     String[] mostFamousWorkCopy = genericDoc.getPropertyStringArray("mostFamousWork");
     String mostFamousWorkConv = null;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListSimple.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListSimple.JAVA
index d7fe77d..3f30bc6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListSimple.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListSimple.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -64,7 +64,7 @@
 
   @Override
   public Person fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
@@ -75,7 +75,7 @@
     GenericDocument livesAtCopy = genericDoc.getPropertyDocument("livesAt");
     Address livesAtConv = null;
     if (livesAtCopy != null) {
-      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMap);
+      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMappingContext);
     }
     Person document = new Person();
     document.namespace = namespaceConv;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListTopLevelInheritTrue.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListTopLevelInheritTrue.JAVA
index 821dcc4..2fd3487 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListTopLevelInheritTrue.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexableNestedPropertiesListTopLevelInheritTrue.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -87,7 +87,7 @@
 
   @Override
   public ArtistEmployee fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] nameCopy = genericDoc.getPropertyStringArray("name");
@@ -98,7 +98,7 @@
     GenericDocument livesAtCopy = genericDoc.getPropertyDocument("livesAt");
     Address livesAtConv = null;
     if (livesAtCopy != null) {
-      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMap);
+      livesAtConv = livesAtCopy.toDocumentClass(Address.class, documentClassMappingContext);
     }
     String[] mostFamousWorkCopy = genericDoc.getPropertyStringArray("mostFamousWork");
     String mostFamousWorkConv = null;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
index 607d0c3..35d9c68 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -71,7 +71,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] indexNoneCopy = genericDoc.getPropertyStringArray("indexNone");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
index e99bb1d..86d2998 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -51,7 +51,7 @@
 
   @Override
   public Gift.InnerGift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] arrStringConv = genericDoc.getPropertyStringArray("arrString");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceAsNestedDocument.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceAsNestedDocument.JAVA
index f09ac0b..a53873a 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceAsNestedDocument.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceAsNestedDocument.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -52,13 +52,13 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     GenericDocument thingCopy = genericDoc.getPropertyDocument("thing");
     Thing thingConv = null;
     if (thingCopy != null) {
-      thingConv = thingCopy.toDocumentClass(Thing.class, documentClassMap);
+      thingConv = thingCopy.toDocumentClass(Thing.class, documentClassMappingContext);
     }
     Gift document = new Gift();
     document.namespace = namespaceConv;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceImplementingParents.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceImplementingParents.JAVA
index 51c1c41..fdcb2ff 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceImplementingParents.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceImplementingParents.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -71,7 +71,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String getNamespaceConv = genericDoc.getNamespace();
     String getIdConv = genericDoc.getId();
     String[] getStr2Copy = genericDoc.getPropertyStringArray("str2");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
index 0f7d12b..4cbe348 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -11,7 +12,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -129,7 +129,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     long[] defaultIndexNoneCopy = genericDoc.getPropertyLongArray("defaultIndexNone");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongSerializer.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongSerializer.JAVA
index 9fe4b74..b16c61c4 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongSerializer.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongSerializer.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -10,7 +11,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -79,7 +79,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String mIdConv = genericDoc.getId();
     String mNamespaceConv = genericDoc.getNamespace();
     long mPricePointCopy = genericDoc.getPropertyLong("pricePoint");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNestedAutoValueDocument.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNestedAutoValueDocument.JAVA
index 3ab65b5..be4a17c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNestedAutoValueDocument.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNestedAutoValueDocument.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -41,7 +41,7 @@
 
   @Override
   public Gift.A fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     Gift.A document = Gift.A.create(idConv, namespaceConv);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNesting.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNesting.JAVA
index 7712cb3..57c9576 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNesting.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNesting.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -61,18 +61,18 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String idConv = genericDoc.getId();
     String namespaceConv = genericDoc.getNamespace();
     GenericDocument middleContentACopy = genericDoc.getPropertyDocument("middleContentA");
     Middle middleContentAConv = null;
     if (middleContentACopy != null) {
-      middleContentAConv = middleContentACopy.toDocumentClass(Middle.class, documentClassMap);
+      middleContentAConv = middleContentACopy.toDocumentClass(Middle.class, documentClassMappingContext);
     }
     GenericDocument middleContentBCopy = genericDoc.getPropertyDocument("middleContentB");
     Middle middleContentBConv = null;
     if (middleContentBCopy != null) {
-      middleContentBConv = middleContentBCopy.toDocumentClass(Middle.class, documentClassMap);
+      middleContentBConv = middleContentBCopy.toDocumentClass(Middle.class, documentClassMappingContext);
     }
     Gift document = new Gift();
     document.id = idConv;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNameNormalization.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNameNormalization.JAVA
index 54cbc1b..f8866eb 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNameNormalization.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNameNormalization.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -60,7 +60,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int getMPriceConv = (int) genericDoc.getPropertyLong("mPrice");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNestedDocumentsIndexing.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNestedDocumentsIndexing.JAVA
index bccc026..7a71022 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNestedDocumentsIndexing.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNestedDocumentsIndexing.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -10,7 +11,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -112,7 +112,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String idConv = genericDoc.getId();
     String namespaceConv = genericDoc.getNamespace();
     GenericDocument[] giftContentsCollectionCopy = genericDoc.getPropertyDocumentArray("giftContentsCollection");
@@ -120,7 +120,7 @@
     if (giftContentsCollectionCopy != null) {
       giftContentsCollectionConv = new ArrayList<>(giftContentsCollectionCopy.length);
       for (int i = 0; i < giftContentsCollectionCopy.length; i++) {
-        giftContentsCollectionConv.add(giftContentsCollectionCopy[i].toDocumentClass(GiftContent.class, documentClassMap));
+        giftContentsCollectionConv.add(giftContentsCollectionCopy[i].toDocumentClass(GiftContent.class, documentClassMappingContext));
       }
     }
     GenericDocument[] giftContentsArrayCopy = genericDoc.getPropertyDocumentArray("giftContentsArray");
@@ -128,20 +128,20 @@
     if (giftContentsArrayCopy != null) {
       giftContentsArrayConv = new GiftContent[giftContentsArrayCopy.length];
       for (int i = 0; i < giftContentsArrayCopy.length; i++) {
-        giftContentsArrayConv[i] = giftContentsArrayCopy[i].toDocumentClass(GiftContent.class, documentClassMap);
+        giftContentsArrayConv[i] = giftContentsArrayCopy[i].toDocumentClass(GiftContent.class, documentClassMappingContext);
       }
     }
     GenericDocument giftContentCopy = genericDoc.getPropertyDocument("giftContent");
     GiftContent giftContentConv = null;
     if (giftContentCopy != null) {
-      giftContentConv = giftContentCopy.toDocumentClass(GiftContent.class, documentClassMap);
+      giftContentConv = giftContentCopy.toDocumentClass(GiftContent.class, documentClassMappingContext);
     }
     GenericDocument[] giftContentsCollectionNotIndexedCopy = genericDoc.getPropertyDocumentArray("giftContentsCollectionNotIndexed");
     List<GiftContent> giftContentsCollectionNotIndexedConv = null;
     if (giftContentsCollectionNotIndexedCopy != null) {
       giftContentsCollectionNotIndexedConv = new ArrayList<>(giftContentsCollectionNotIndexedCopy.length);
       for (int i = 0; i < giftContentsCollectionNotIndexedCopy.length; i++) {
-        giftContentsCollectionNotIndexedConv.add(giftContentsCollectionNotIndexedCopy[i].toDocumentClass(GiftContent.class, documentClassMap));
+        giftContentsCollectionNotIndexedConv.add(giftContentsCollectionNotIndexedCopy[i].toDocumentClass(GiftContent.class, documentClassMappingContext));
       }
     }
     GenericDocument[] giftContentsArrayNotIndexedCopy = genericDoc.getPropertyDocumentArray("giftContentsArrayNotIndexed");
@@ -149,13 +149,13 @@
     if (giftContentsArrayNotIndexedCopy != null) {
       giftContentsArrayNotIndexedConv = new GiftContent[giftContentsArrayNotIndexedCopy.length];
       for (int i = 0; i < giftContentsArrayNotIndexedCopy.length; i++) {
-        giftContentsArrayNotIndexedConv[i] = giftContentsArrayNotIndexedCopy[i].toDocumentClass(GiftContent.class, documentClassMap);
+        giftContentsArrayNotIndexedConv[i] = giftContentsArrayNotIndexedCopy[i].toDocumentClass(GiftContent.class, documentClassMappingContext);
       }
     }
     GenericDocument giftContentNotIndexedCopy = genericDoc.getPropertyDocument("giftContentNotIndexed");
     GiftContent giftContentNotIndexedConv = null;
     if (giftContentNotIndexedCopy != null) {
-      giftContentNotIndexedConv = giftContentNotIndexedCopy.toDocumentClass(GiftContent.class, documentClassMap);
+      giftContentNotIndexedConv = giftContentNotIndexedCopy.toDocumentClass(GiftContent.class, documentClassMappingContext);
     }
     Gift document = new Gift();
     document.id = idConv;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOneBadConstructor.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOneBadConstructor.JAVA
index 0bece60..24958a6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOneBadConstructor.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOneBadConstructor.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -41,7 +41,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String mNamespaceConv = genericDoc.getNamespace();
     String mIdConv = genericDoc.getId();
     Gift document = new Gift(mIdConv);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOverloadedGetterIsOk.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOverloadedGetterIsOk.JAVA
index 89a4f42..349343c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOverloadedGetterIsOk.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOverloadedGetterIsOk.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphism.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphism.JAVA
index 4bc175c..1a3d212a 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphism.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphism.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -76,7 +76,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] note2Copy = genericDoc.getPropertyStringArray("note2");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismDuplicatedParents.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismDuplicatedParents.JAVA
index 4bc175c..1a3d212a 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismDuplicatedParents.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismDuplicatedParents.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -76,7 +76,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] note2Copy = genericDoc.getPropertyStringArray("note2");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismOverrideExtendedProperty.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismOverrideExtendedProperty.JAVA
index 91f4581..ff45ac2 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismOverrideExtendedProperty.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismOverrideExtendedProperty.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -76,7 +76,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] note2Copy = genericDoc.getPropertyStringArray("note2");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismWithNestedType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismWithNestedType.JAVA
index 975b98a..f00d9b1 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismWithNestedType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismWithNestedType.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -86,7 +86,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] note2Copy = genericDoc.getPropertyStringArray("note2");
@@ -107,7 +107,7 @@
     GenericDocument innerContentCopy = genericDoc.getPropertyDocument("innerContent");
     Inner innerContentConv = null;
     if (innerContentCopy != null) {
-      innerContentConv = innerContentCopy.toDocumentClass(Inner.class, documentClassMap);
+      innerContentConv = innerContentCopy.toDocumentClass(Inner.class, documentClassMappingContext);
     }
     Gift document = new Gift();
     document.namespace = namespaceConv;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
index 1e045ae..4e279be 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -51,7 +51,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] oldNameCopy = genericDoc.getPropertyStringArray("newName");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyNamedAsDocumentClassMap.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyNamedAsDocumentClassMap.JAVA
index 3059558..bf19f33 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyNamedAsDocumentClassMap.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyNamedAsDocumentClassMap.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int documentClassMapConv = (int) genericDoc.getPropertyLong("documentClassMap");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_GetterReturnsSubtype.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_GetterReturnsSubtype.JAVA
index dfadca5..bf96697 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_GetterReturnsSubtype.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_GetterReturnsSubtype.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -10,7 +11,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -53,7 +53,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] fromCopy = genericDoc.getPropertyStringArray("from");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
index 0360605..23842cb 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int priceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_isGetterForBoolean.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_isGetterForBoolean.JAVA
index 4d9acea..ba1128d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_isGetterForBoolean.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_isGetterForBoolean.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -45,7 +45,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     boolean forSaleConv = genericDoc.getPropertyBoolean("forSale");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
index 810bd5f..da808de 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -13,7 +14,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -83,7 +83,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] listOfStringCopy = genericDoc.getPropertyStringArray("listOfString");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSameNameGetterAndFieldAnnotatingField.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSameNameGetterAndFieldAnnotatingField.JAVA
index 2ba3fce..c6bfdd3 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSameNameGetterAndFieldAnnotatingField.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSameNameGetterAndFieldAnnotatingField.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int priceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSameNameGetterAndFieldAnnotatingGetter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSameNameGetterAndFieldAnnotatingGetter.JAVA
index 89a4f42..349343c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSameNameGetterAndFieldAnnotatingGetter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSameNameGetterAndFieldAnnotatingGetter.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int getPriceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringPropertyJoinableType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringPropertyJoinableType.JAVA
index 2393da2..5be02c3 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringPropertyJoinableType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringPropertyJoinableType.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -51,7 +51,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] objectCopy = genericDoc.getPropertyStringArray("object");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringSerializer.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringSerializer.JAVA
index d458eed..aa99fc1 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringSerializer.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringSerializer.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -11,7 +12,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -86,7 +86,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String mIdConv = genericDoc.getId();
     String mNamespaceConv = genericDoc.getNamespace();
     String mUrlCopy = genericDoc.getPropertyString("url");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
index b9f09a0..35d4caa 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -54,7 +54,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int priceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass.JAVA
index 2306f74..df92190 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -66,7 +66,7 @@
 
   @Override
   public Gift.FooGift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int scoreConv = genericDoc.getScore();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassPojoAncestor.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassPojoAncestor.JAVA
index 3f68125..b818f8c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassPojoAncestor.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassPojoAncestor.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -66,7 +66,7 @@
 
   @Override
   public Gift.FooGift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int scoreConv = genericDoc.getScore();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassWithPrivateFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassWithPrivateFields.JAVA
index 77777e8..d4bd15b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassWithPrivateFields.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassWithPrivateFields.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -71,7 +71,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String mNamespaceConv = genericDoc.getNamespace();
     String mIdConv = genericDoc.getId();
     String[] mNoteCopy = genericDoc.getPropertyStringArray("note");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_changeSchemaName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_changeSchemaName.JAVA
index 56dfdb6..900b6bc 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_changeSchemaName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_changeSchemaName.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -61,7 +61,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] noteCopy = genericDoc.getPropertyStringArray("note");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_multipleChangedSchemaNames.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_multipleChangedSchemaNames.JAVA
index 457b1071..2b00ef2 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_multipleChangedSchemaNames.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_multipleChangedSchemaNames.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -65,7 +65,7 @@
 
   @Override
   public FooGift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] noteCopy = genericDoc.getPropertyStringArray("note");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
index 6ac5c27..63e3ae6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -17,7 +18,6 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -390,7 +390,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     long[] collectLongCopy = genericDoc.getPropertyLongArray("collectLong");
@@ -451,7 +451,7 @@
     if (collectGiftCopy != null) {
       collectGiftConv = new ArrayList<>(collectGiftCopy.length);
       for (int i = 0; i < collectGiftCopy.length; i++) {
-        collectGiftConv.add(collectGiftCopy[i].toDocumentClass(Gift.class, documentClassMap));
+        collectGiftConv.add(collectGiftCopy[i].toDocumentClass(Gift.class, documentClassMappingContext));
       }
     }
     EmbeddingVector[] collectVecCopy = genericDoc.getPropertyEmbeddingArray("collectVec");
@@ -525,7 +525,7 @@
     if (arrGiftCopy != null) {
       arrGiftConv = new Gift[arrGiftCopy.length];
       for (int i = 0; i < arrGiftCopy.length; i++) {
-        arrGiftConv[i] = arrGiftCopy[i].toDocumentClass(Gift.class, documentClassMap);
+        arrGiftConv[i] = arrGiftCopy[i].toDocumentClass(Gift.class, documentClassMappingContext);
       }
     }
     EmbeddingVector[] arrVecConv = genericDoc.getPropertyEmbeddingArray("arrVec");
@@ -572,7 +572,7 @@
     GenericDocument giftCopy = genericDoc.getPropertyDocument("gift");
     Gift giftConv = null;
     if (giftCopy != null) {
-      giftConv = giftCopy.toDocumentClass(Gift.class, documentClassMap);
+      giftConv = giftCopy.toDocumentClass(Gift.class, documentClassMappingContext);
     }
     EmbeddingVector[] vecCopy = genericDoc.getPropertyEmbeddingArray("vec");
     EmbeddingVector vecConv = null;
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
index 6425984..ab1eaa1 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -161,7 +161,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     String[] tokNoneInvalidCopy = genericDoc.getPropertyStringArray("tokNoneInvalid");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA
index c3ddb1f..34e5321 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -61,7 +61,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String id_Conv = genericDoc.getId();
     int price1Conv = (int) genericDoc.getPropertyLong("price1");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
index 0360605..23842cb 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int priceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA
index ca916cf..027282b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.DocumentClassMappingContext;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Class;
@@ -9,7 +10,6 @@
 import java.lang.String;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import javax.annotation.processing.Generated;
 
 @Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -46,7 +46,7 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc,
-      Map<String, List<String>> documentClassMap) throws AppSearchException {
+      DocumentClassMappingContext documentClassMappingContext) throws AppSearchException {
     String namespaceConv = genericDoc.getNamespace();
     String idConv = genericDoc.getId();
     int priceConv = (int) genericDoc.getPropertyLong("price");
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
index 58c355a..6cf5746 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
@@ -22,15 +22,31 @@
 import android.content.Intent
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CameraExtensionCharacteristics
 import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback
 import android.hardware.camera2.CameraManager
 import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_TRIGGER_CANCEL
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_TRIGGER_IDLE
 import android.hardware.camera2.CaptureFailure
 import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE
+import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE_ON
+import android.hardware.camera2.CaptureRequest.CONTROL_AE_REGIONS
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_MODE
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_MODE_AUTO
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_REGIONS
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_START
+import android.hardware.camera2.CaptureRequest.CONTROL_AWB_REGIONS
+import android.hardware.camera2.CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE
+import android.hardware.camera2.TotalCaptureResult
 import android.hardware.camera2.params.ExtensionSessionConfiguration
+import android.hardware.camera2.params.MeteringRectangle
 import android.hardware.camera2.params.OutputConfiguration
 import android.hardware.camera2.params.SessionConfiguration
 import android.hardware.camera2.params.SessionConfiguration.SESSION_REGULAR
@@ -68,6 +84,7 @@
 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES
 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_IMAGE_URI
 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_REQUEST_CODE
+import androidx.camera.integration.extensions.TapToFocusDetector.CameraInfo
 import androidx.camera.integration.extensions.TestResultType.TEST_RESULT_FAILED
 import androidx.camera.integration.extensions.TestResultType.TEST_RESULT_NOT_TESTED
 import androidx.camera.integration.extensions.TestResultType.TEST_RESULT_PASSED
@@ -152,6 +169,9 @@
      */
     private var cameraCaptureSession: Any? = null
 
+    private val focusMeteringControl = FocusMeteringControl(::startAfTrigger, ::cancelAfTrigger)
+    private var meteringRectangles: Array<MeteringRectangle?> = EMPTY_RECTANGLES
+
     // ===============================================================
     // Fields that will be accessed on the camera thread
     // ===============================================================
@@ -172,6 +192,8 @@
     private val supportedExtensionModes = mutableListOf<Int>()
     private var extensionModeEnabled = false
 
+    private lateinit var tapToFocusDetector: TapToFocusDetector
+
     // ===============================================================
     // Fields that will be accessed under synchronization protection
     // ===============================================================
@@ -254,18 +276,13 @@
             }
         }
 
-    private val captureCallbacks =
-        object : CameraExtensionSession.ExtensionCaptureCallback() {
+    private val captureCallbackExtensionMode =
+        object : ExtensionCaptureCallback() {
             override fun onCaptureProcessStarted(
                 session: CameraExtensionSession,
                 request: CaptureRequest
             ) {
-                if (
-                    receivedCaptureProcessStartedCount.getAndIncrement() >=
-                        FRAMES_UNTIL_VIEW_IS_READY && !captureProcessStartedIdlingResource.isIdleNow
-                ) {
-                    captureProcessStartedIdlingResource.decrement()
-                }
+                handleCaptureStartedEvent()
             }
 
             override fun onCaptureFailed(session: CameraExtensionSession, request: CaptureRequest) {
@@ -273,23 +290,36 @@
             }
         }
 
-    private val captureCallbacksNormalMode =
-        object : CameraCaptureSession.CaptureCallback() {
+    private val captureCallbackNormalMode =
+        object : CaptureCallback() {
             override fun onCaptureStarted(
                 session: CameraCaptureSession,
                 request: CaptureRequest,
                 timestamp: Long,
                 frameNumber: Long
             ) {
-                if (
-                    receivedCaptureProcessStartedCount.getAndIncrement() >=
-                        FRAMES_UNTIL_VIEW_IS_READY && !captureProcessStartedIdlingResource.isIdleNow
-                ) {
-                    captureProcessStartedIdlingResource.decrement()
-                }
+                handleCaptureStartedEvent()
             }
         }
 
+    private fun handleCaptureStartedEvent() {
+        checkRunOnCameraThread()
+        if (
+            receivedCaptureProcessStartedCount.getAndIncrement() >= FRAMES_UNTIL_VIEW_IS_READY &&
+                !captureProcessStartedIdlingResource.isIdleNow
+        ) {
+            captureProcessStartedIdlingResource.decrement()
+        }
+    }
+
+    private val comboCaptureCallbackExtensionMode =
+        ComboCaptureCallbackExtensionMode().apply {
+            addCaptureCallback(captureCallbackExtensionMode)
+        }
+
+    private val comboCaptureCallbackNormalMode =
+        ComboCaptureCallbackNormalMode().apply { addCaptureCallback(captureCallbackNormalMode) }
+
     private val cameraTaskDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
     private lateinit var cameraThread: Thread
 
@@ -443,7 +473,7 @@
         enableUiControl(false)
         setupUiControl()
         setupVideoStabilizationModeView()
-        enableZoomGesture()
+        enableZoomAndTapToFocusGesture()
     }
 
     private fun setupForRequestMode() {
@@ -620,10 +650,12 @@
         findViewById<Button>(R.id.Picture).isEnabled = enabled
     }
 
-    private fun enableZoomGesture() {
+    private fun enableZoomAndTapToFocusGesture() {
         val scaleGestureDetector = ScaleGestureDetector(this, scaleGestureListener)
         textureView.setOnTouchListener { _, event ->
-            event != null && scaleGestureDetector.onTouchEvent(event)
+            scaleGestureDetector.onTouchEvent(event)
+            tapToFocusDetector.onTouchEvent(event)
+            true
         }
     }
 
@@ -895,6 +927,30 @@
             cameraSensorRotationDegrees,
             lensFacing == CameraCharacteristics.LENS_FACING_BACK
         )
+
+        tapToFocusDetector =
+            TapToFocusDetector(this, textureView, getCameraInfo(), display!!.rotation, ::tapToFocus)
+    }
+
+    private fun getCameraInfo(): CameraInfo {
+        checkRunOnMainThread()
+        val lensFacing =
+            cameraManager
+                .getCameraCharacteristics(currentCameraId)[CameraCharacteristics.LENS_FACING]
+        val sensorOrientation =
+            cameraManager
+                .getCameraCharacteristics(currentCameraId)[CameraCharacteristics.SENSOR_ORIENTATION]
+        val activeArraySize =
+            cameraManager
+                .getCameraCharacteristics(currentCameraId)[
+                    CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]
+        return CameraInfo(lensFacing!!, sensorOrientation!!.toFloat(), activeArraySize!!)
+    }
+
+    private fun tapToFocus(meteringRectangles: Array<MeteringRectangle?>) {
+        coroutineScope.launch(cameraTaskDispatcher) {
+            focusMeteringControl.updateMeteringRectangles(meteringRectangles)
+        }
     }
 
     private fun checkRunOnMainThread() {
@@ -907,7 +963,10 @@
     }
 
     private fun checkRunOnCameraThread() {
-        if (Thread.currentThread() != cameraThread) {
+        if (
+            Thread.currentThread() != cameraThread &&
+                Thread.currentThread() != normalModeCaptureThread
+        ) {
             val exception = IllegalStateException("Must run on the camera thread!")
             Log.e(TAG, exception.toString())
             exception.printStackTrace()
@@ -1018,6 +1077,8 @@
     private fun openCaptureSession(extensionMode: Int, extensionModeEnabled: Boolean) =
         coroutineScope.async(cameraTaskDispatcher) {
             Log.d(TAG, "openCaptureSession")
+            // Resets the metering rectangles
+            meteringRectangles = EMPTY_RECTANGLES
             setCurrentState(STATE_CAPTURE_SESSION_OPENING)
 
             if (stillImageReader != null) {
@@ -1134,39 +1195,109 @@
         closeCamera()
     }
 
-    private fun setRepeatingRequest() {
+    private fun startAfTrigger(meteringRectangles: Array<MeteringRectangle?>) {
         coroutineScope.launch(cameraTaskDispatcher) {
-            val captureBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
-            captureBuilder.addTarget(previewSurface!!)
-            val videoStabilizationMode =
-                if (videoStabilizationToggleView.isChecked) {
-                    CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
-                } else {
-                    CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF
-                }
+            [email protected] = meteringRectangles
+            addFocusMeteringCaptureCallback()
 
-            captureBuilder.set(
-                CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
-                videoStabilizationMode
+            val captureBuilder = getCaptureRequestBuilder()
+
+            captureBuilder.set(CONTROL_AF_TRIGGER, CONTROL_AF_TRIGGER_START)
+
+            setRepeatingRequest(captureBuilder.build())
+        }
+    }
+
+    private fun cancelAfTrigger(afTriggerType: Int) {
+        coroutineScope.launch(cameraTaskDispatcher) {
+            if (afTriggerType == CONTROL_AF_TRIGGER_CANCEL) {
+                [email protected] = EMPTY_RECTANGLES
+            }
+
+            removeFocusMeteringCaptureCallback()
+
+            val captureBuilder = getCaptureRequestBuilder()
+
+            if (afTriggerType == CONTROL_AF_TRIGGER_IDLE) {
+                captureBuilder.set(CONTROL_AF_TRIGGER, CONTROL_AF_TRIGGER_IDLE)
+            } else {
+                captureBuilder.set(CONTROL_AF_TRIGGER, CONTROL_AF_TRIGGER_CANCEL)
+            }
+
+            setRepeatingRequest(captureBuilder.build())
+        }
+    }
+
+    private fun addFocusMeteringCaptureCallback() {
+        checkRunOnCameraThread()
+        val captureCallback =
+            focusMeteringControl.getCaptureCallback(cameraCaptureSession is CameraExtensionSession)
+        if (cameraCaptureSession is CameraExtensionSession) {
+            comboCaptureCallbackExtensionMode.addCaptureCallback(
+                captureCallback as ExtensionCaptureCallback
             )
+        } else {
+            comboCaptureCallbackNormalMode.addCaptureCallback(captureCallback as CaptureCallback)
+        }
+    }
 
-            captureBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio)
+    private fun removeFocusMeteringCaptureCallback() {
+        checkRunOnCameraThread()
+        val captureCallback =
+            focusMeteringControl.getCaptureCallback(cameraCaptureSession is CameraExtensionSession)
+        if (cameraCaptureSession is CameraExtensionSession) {
+            comboCaptureCallbackExtensionMode.removeCaptureCallback(
+                captureCallback as ExtensionCaptureCallback
+            )
+        } else {
+            comboCaptureCallbackNormalMode.removeCaptureCallback(captureCallback as CaptureCallback)
+        }
+    }
+
+    private fun setRepeatingRequest(captureRequest: CaptureRequest? = null) {
+        coroutineScope.launch(cameraTaskDispatcher) {
             if (cameraCaptureSession is CameraCaptureSession) {
                 (cameraCaptureSession as CameraCaptureSession).setRepeatingRequest(
-                    captureBuilder.build(),
-                    captureCallbacksNormalMode,
+                    captureRequest ?: getCaptureRequestBuilder().build(),
+                    comboCaptureCallbackNormalMode,
                     normalModeCaptureHandler
                 )
             } else {
                 (cameraCaptureSession as CameraExtensionSession).setRepeatingRequest(
-                    captureBuilder.build(),
+                    captureRequest ?: getCaptureRequestBuilder().build(),
                     cameraTaskDispatcher.asExecutor(),
-                    captureCallbacks
+                    comboCaptureCallbackExtensionMode
                 )
             }
         }
     }
 
+    private fun getCaptureRequestBuilder(): CaptureRequest.Builder {
+        checkRunOnCameraThread()
+        val captureBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+        captureBuilder.addTarget(previewSurface!!)
+        val videoStabilizationMode =
+            if (videoStabilizationToggleView.isChecked) {
+                CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
+            } else {
+                CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF
+            }
+
+        captureBuilder.set(CONTROL_VIDEO_STABILIZATION_MODE, videoStabilizationMode)
+
+        captureBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio)
+
+        if (!meteringRectangles.contentEquals(EMPTY_RECTANGLES)) {
+            captureBuilder.set(CONTROL_AF_MODE, CONTROL_AF_MODE_AUTO)
+            captureBuilder.set(CONTROL_AF_REGIONS, meteringRectangles)
+            captureBuilder.set(CONTROL_AE_MODE, CONTROL_AE_MODE_ON)
+            captureBuilder.set(CONTROL_AE_REGIONS, meteringRectangles)
+            captureBuilder.set(CONTROL_AWB_REGIONS, meteringRectangles)
+        }
+
+        return captureBuilder
+    }
+
     private fun setupImageReader(cameraId: String, extensionMode: Int): ImageReader {
         val (size, format) =
             pickStillImageResolution(
@@ -1308,7 +1439,7 @@
             if (cameraCaptureSession is CameraCaptureSession) {
                 (cameraCaptureSession as CameraCaptureSession).capture(
                     captureBuilder.build(),
-                    object : CameraCaptureSession.CaptureCallback() {
+                    object : CaptureCallback() {
                         override fun onCaptureFailed(
                             session: CameraCaptureSession,
                             request: CaptureRequest,
@@ -1324,7 +1455,7 @@
                 (cameraCaptureSession as CameraExtensionSession).capture(
                     captureBuilder.build(),
                     cameraTaskDispatcher.asExecutor(),
-                    object : CameraExtensionSession.ExtensionCaptureCallback() {
+                    object : ExtensionCaptureCallback() {
                         override fun onCaptureFailed(
                             session: CameraExtensionSession,
                             request: CaptureRequest
@@ -1536,6 +1667,89 @@
         fun maxZoom(characteristics: CameraCharacteristics): Float =
             characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)?.upper ?: 1.0f
     }
+
+    /**
+     * A combo ExtensionCaptureCallback implementation to receive to pass the events to the
+     * underlying callbacks.
+     */
+    private class ComboCaptureCallbackExtensionMode : ExtensionCaptureCallback() {
+        private val captureCallbacks: MutableList<ExtensionCaptureCallback> = mutableListOf()
+
+        fun addCaptureCallback(captureCallback: ExtensionCaptureCallback) {
+            if (!captureCallbacks.contains(captureCallback)) {
+                captureCallbacks.add(captureCallback)
+            }
+        }
+
+        fun removeCaptureCallback(captureCallback: ExtensionCaptureCallback) {
+            captureCallbacks.remove(captureCallback)
+        }
+
+        override fun onCaptureStarted(
+            session: CameraExtensionSession,
+            request: CaptureRequest,
+            timestamp: Long
+        ) {
+            captureCallbacks.forEach { it.onCaptureStarted(session, request, timestamp) }
+        }
+
+        override fun onCaptureProcessStarted(
+            session: CameraExtensionSession,
+            request: CaptureRequest
+        ) {
+            captureCallbacks.forEach { it.onCaptureProcessStarted(session, request) }
+        }
+
+        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+        override fun onCaptureResultAvailable(
+            session: CameraExtensionSession,
+            request: CaptureRequest,
+            result: TotalCaptureResult
+        ) {
+            captureCallbacks.forEach { it.onCaptureResultAvailable(session, request, result) }
+        }
+
+        override fun onCaptureFailed(session: CameraExtensionSession, request: CaptureRequest) {
+            captureCallbacks.forEach { it.onCaptureFailed(session, request) }
+        }
+    }
+
+    /**
+     * A combo CaptureCallback implementation to receive to pass the events to the underlying
+     * callbacks.
+     */
+    private class ComboCaptureCallbackNormalMode : CaptureCallback() {
+        private val captureCallbacks: MutableList<CaptureCallback> = mutableListOf()
+
+        fun addCaptureCallback(captureCallback: CaptureCallback) {
+            if (!captureCallbacks.contains(captureCallback)) {
+                captureCallbacks.add(captureCallback)
+            }
+        }
+
+        fun removeCaptureCallback(captureCallback: CaptureCallback) {
+            captureCallbacks.remove(captureCallback)
+        }
+
+        override fun onCaptureStarted(
+            session: CameraCaptureSession,
+            request: CaptureRequest,
+            timestamp: Long,
+            frameNumber: Long
+        ) {
+            captureCallbacks.forEach {
+                it.onCaptureStarted(session, request, timestamp, frameNumber)
+            }
+        }
+
+        override fun onCaptureCompleted(
+            session: CameraCaptureSession,
+            request: CaptureRequest,
+            result: TotalCaptureResult
+        ) {
+            captureCallbacks.forEach { it.onCaptureCompleted(session, request, result) }
+        }
+    }
 }
 
 fun Double.format(scale: Int): String = String.format("%.${scale}f", this)
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/FocusMeteringControl.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/FocusMeteringControl.kt
new file mode 100644
index 0000000..166cee0
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/FocusMeteringControl.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_TRIGGER_CANCEL
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_TRIGGER_IDLE
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.hardware.camera2.params.MeteringRectangle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+val EMPTY_RECTANGLES = arrayOfNulls<MeteringRectangle>(0)
+
+private const val TAG = "FocusMeteringControl"
+private const val AUTO_FOCUS_TIMEOUT_DURATION_MS = 5000L
+
+/**
+ * A class to manage focus-metering related operations. This class will help to monitor whether the
+ * state is locked or not and then make the AF-Trigger become to idle or cancel state.
+ */
+@RequiresApi(31)
+class FocusMeteringControl(
+    private val startAfTriggerImpl: (Array<MeteringRectangle?>) -> Unit,
+    private val cancelAfTriggerImpl: (Int) -> Unit
+) {
+
+    private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
+    private var currentAfState: Int = CaptureResult.CONTROL_AF_STATE_INACTIVE
+    private var autoCancelHandle: ScheduledFuture<*>? = null
+    private var autoFocusTimeoutHandle: ScheduledFuture<*>? = null
+    private var focusTimeoutCounter: Long = 0
+    private var isAutoFocusCompleted: Boolean = true
+
+    private val captureCallbackExtensionMode =
+        object : CameraExtensionSession.ExtensionCaptureCallback() {
+            override fun onCaptureResultAvailable(
+                session: CameraExtensionSession,
+                request: CaptureRequest,
+                result: TotalCaptureResult
+            ) {
+                result.get(CaptureResult.CONTROL_AF_STATE)?.let { handleCaptureResultForAf(it) }
+            }
+        }
+
+    private val captureCallbackNormalMode =
+        object : CameraCaptureSession.CaptureCallback() {
+            override fun onCaptureCompleted(
+                session: CameraCaptureSession,
+                request: CaptureRequest,
+                result: TotalCaptureResult
+            ) {
+                result.get(CaptureResult.CONTROL_AF_STATE)?.let { handleCaptureResultForAf(it) }
+            }
+        }
+
+    private fun handleCaptureResultForAf(afState: Int?) {
+        if (isAutoFocusCompleted) {
+            return
+        }
+
+        if (afState == null) {
+            Log.e(TAG, "afState == null")
+            // set isAutoFocusCompleted to true when camera does not support AF_AUTO.
+            isAutoFocusCompleted = true
+        } else if (currentAfState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN) {
+            if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED) {
+                Log.d(TAG, "afState == CONTROL_AF_STATE_FOCUSED_LOCKED")
+                isAutoFocusCompleted = true
+            } else if (afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
+                Log.d(TAG, "afState == CONTROL_AF_STATE_NOT_FOCUSED_LOCKED")
+                isAutoFocusCompleted = true
+            }
+        }
+
+        // Check 3A regions
+        if (isAutoFocusCompleted) {
+            clearAutoFocusTimeoutHandle()
+            Log.d(TAG, "cancelAfTrigger: CONTROL_AF_TRIGGER_IDLE")
+            cancelAfTriggerImpl.invoke(CONTROL_AF_TRIGGER_IDLE)
+        }
+
+        if (currentAfState != afState && afState != null) {
+            currentAfState = afState
+        }
+    }
+
+    fun updateMeteringRectangles(meteringRectangles: Array<MeteringRectangle?>) {
+        clearAutoFocusTimeoutHandle()
+        isAutoFocusCompleted = false
+        val timeoutId: Long = ++focusTimeoutCounter
+        autoFocusTimeoutHandle =
+            scheduler.schedule(
+                {
+                    Log.d(TAG, "cancelAfTrigger: CONTROL_AF_TRIGGER_CANCEL")
+                    cancelAfTriggerImpl.invoke(CONTROL_AF_TRIGGER_CANCEL)
+                    if (timeoutId == focusTimeoutCounter) {
+                        isAutoFocusCompleted = true
+                    }
+                },
+                AUTO_FOCUS_TIMEOUT_DURATION_MS,
+                TimeUnit.MILLISECONDS
+            )
+        currentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE
+        startAfTriggerImpl.invoke(meteringRectangles)
+    }
+
+    fun getCaptureCallback(extensionEnabled: Boolean): Any =
+        if (extensionEnabled) {
+            captureCallbackExtensionMode
+        } else {
+            captureCallbackNormalMode
+        }
+
+    private fun clearAutoFocusTimeoutHandle() {
+        autoFocusTimeoutHandle?.let {
+            it.cancel(/* mayInterruptIfRunning= */ true)
+            autoCancelHandle = null
+        }
+    }
+}
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/TapToFocusDetector.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/TapToFocusDetector.kt
new file mode 100644
index 0000000..eb4cc73
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/TapToFocusDetector.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions
+
+import android.content.Context
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.MeteringRectangle
+import android.util.Log
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.Surface
+import android.view.TextureView
+import androidx.core.math.MathUtils.clamp
+
+private const val TAG = "TapToFocusDetector"
+private const val METERING_RECTANGLE_SIZE = 0.15f
+
+/**
+ * A class helps to detect the tap-to-focus event and also normalize the point to mapping to the
+ * camera sensor coordinate.
+ */
+class TapToFocusDetector(
+    context: Context,
+    private val textureView: TextureView,
+    private val cameraInfo: CameraInfo,
+    private val displayRotation: Int,
+    private val tapToFocusImpl: (Array<MeteringRectangle?>) -> Unit
+) {
+    private val mTapToFocusListener: GestureDetector.SimpleOnGestureListener =
+        object : GestureDetector.SimpleOnGestureListener() {
+            override fun onSingleTapUp(motionEvent: MotionEvent): Boolean {
+                return tapToFocus(motionEvent)
+            }
+        }
+
+    private val tapToFocusGestureDetector = GestureDetector(context, mTapToFocusListener)
+
+    fun onTouchEvent(event: MotionEvent) {
+        tapToFocusGestureDetector.onTouchEvent(event)
+    }
+
+    private fun tapToFocus(motionEvent: MotionEvent): Boolean {
+        val normalizedPoint = calculateCameraSensorMappingPoint(motionEvent)
+        val meteringRectangle = calculateMeteringRectangle(normalizedPoint)
+        tapToFocusImpl.invoke(arrayOf(meteringRectangle))
+        return true
+    }
+
+    /**
+     * Calculates the point which will be mapped to a point in the camera sensor coordinate
+     * dimension.
+     */
+    private fun calculateCameraSensorMappingPoint(motionEvent: MotionEvent): FloatArray {
+        // Gets the dimension info to calculate the normalized point info in the camera sensor
+        // coordinate dimension first.
+        val activeArraySize = cameraInfo.activeArraySize
+        val relativeRotationDegrees = calculateRelativeRotationDegrees()
+        val dimension =
+            if (relativeRotationDegrees % 180 == 0) {
+                activeArraySize
+            } else {
+                Rect(0, 0, activeArraySize.height(), activeArraySize.width())
+            }
+
+        // Calculates what should the full dimension be because the preview might be cropped from
+        // the full FOV of camera sensor.
+        val scaledFullDimension =
+            if (
+                dimension.width() / dimension.height().toFloat() >
+                    textureView.width / textureView.height.toFloat()
+            ) {
+                Rect(
+                    0,
+                    0,
+                    dimension.width() * textureView.height / dimension.height(),
+                    textureView.height
+                )
+            } else {
+                Rect(
+                    0,
+                    0,
+                    textureView.width,
+                    dimension.height() * textureView.width / dimension.width()
+                )
+            }
+
+        // Calculates the shift values for calibration.
+        val shiftX = (scaledFullDimension.width() - textureView.width) / 2
+        val shiftY = (scaledFullDimension.height() - textureView.height) / 2
+
+        // Calculates the normalized point which will be the point between [0, 0] to [1, 1].
+        val normalizedPoint =
+            floatArrayOf(
+                (motionEvent.x + shiftX) / scaledFullDimension.width(),
+                (motionEvent.y + shiftY) / scaledFullDimension.height()
+            )
+
+        // Transforms the normalizedPoint to the camera sensor coordinate.
+        val matrix = Matrix()
+        // Rotates the normalized point to the camera sensor orientation
+        matrix.postRotate(-relativeRotationDegrees.toFloat(), 0.5f, 0.5f)
+        // Flips if current working camera is front camera
+        if (cameraInfo.lensFacing == CameraCharacteristics.LENS_FACING_FRONT) {
+            matrix.postScale(1.0f, -1.0f, 0.5f, 0.5f)
+        }
+        // Scales the point to the camera sensor coordinate dimension.
+        matrix.postScale(activeArraySize.width().toFloat(), activeArraySize.height().toFloat())
+        matrix.mapPoints(normalizedPoint)
+
+        Log.e(TAG, "Tap-to-focus point: ${normalizedPoint.toList()}")
+
+        return normalizedPoint
+    }
+
+    private fun calculateRelativeRotationDegrees(): Int {
+        val rotationDegrees =
+            when (displayRotation) {
+                Surface.ROTATION_0 -> 0
+                Surface.ROTATION_90 -> 90
+                Surface.ROTATION_180 -> 180
+                Surface.ROTATION_270 -> 270
+                else ->
+                    throw IllegalArgumentException("Unsupported surface rotation: $displayRotation")
+            }
+        return if (cameraInfo.lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
+            (cameraInfo.sensorOrientation.toInt() - rotationDegrees + 360) % 360
+        } else {
+            (cameraInfo.sensorOrientation.toInt() + rotationDegrees) % 360
+        }
+    }
+
+    /**
+     * Calculates the metering rectangle according to the camera sensor coordinate dimension mapping
+     * point.
+     */
+    private fun calculateMeteringRectangle(point: FloatArray): MeteringRectangle {
+        val activeArraySize = cameraInfo.activeArraySize
+        val halfMeteringRectWidth: Float = (METERING_RECTANGLE_SIZE * activeArraySize.width()) / 2
+        val halfMeteringRectHeight: Float = (METERING_RECTANGLE_SIZE * activeArraySize.height()) / 2
+
+        val meteringRegion =
+            Rect(
+                clamp((point[0] - halfMeteringRectWidth).toInt(), 0, activeArraySize.width()),
+                clamp((point[1] - halfMeteringRectHeight).toInt(), 0, activeArraySize.height()),
+                clamp((point[0] + halfMeteringRectWidth).toInt(), 0, activeArraySize.width()),
+                clamp((point[1] + halfMeteringRectHeight).toInt(), 0, activeArraySize.height())
+            )
+
+        return MeteringRectangle(meteringRegion, MeteringRectangle.METERING_WEIGHT_MAX)
+    }
+
+    data class CameraInfo(
+        val lensFacing: Int,
+        val sensorOrientation: Float,
+        val activeArraySize: Rect
+    )
+}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index bd53d86..f4ba5f7c 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -48,6 +48,15 @@
             </intent-filter>
         </activity>
         <activity
+            android:name=".InsightActivity"
+            android:exported="true"
+            android:label="C Insight">
+            <intent-filter>
+                <action android:name="androidx.compose.integration.macrobenchmark.target.INSIGHT_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
             android:name=".StaticScrollingContentWithChromeInitialCompositionActivity"
             android:exported="true"
             android:label="C StaticScrollingWithChrome Init">
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/InsightActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/InsightActivity.kt
new file mode 100644
index 0000000..74ecb07
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/InsightActivity.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark.target
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material.Text
+import androidx.compose.ui.util.trace
+
+class InsightActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        // artificial trace sections + sleeps to trigger artificial Insights. If these fail to
+        // trigger insights, see src/trace_processor/metrics/sql/android/android_startup.sql
+        trace("ResourcesManager#getResources") { // duration based insight
+            trace("inflate") { // duration based insight
+                // currently sleep works for this, but spin loop may more accurately simulate work
+                // if this breaks
+                @Suppress("BanThreadSleep") Thread.sleep(500)
+
+                repeat(50) { // count based insight
+                    trace("Broadcast dispatched SOMETHING") {}
+                }
+                repeat(100) { // count based insight
+                    trace("broadcastReceiveReginald") {}
+                }
+            }
+        }
+
+        setContent { Text("Compose Macrobenchmark Target") }
+    }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/InsightBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/InsightBenchmark.kt
new file mode 100644
index 0000000..c021e09
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/InsightBenchmark.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark
+
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.testutils.measureStartup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class InsightBenchmark {
+    @get:Rule val benchmarkRule = MacrobenchmarkRule()
+
+    @Test
+    fun startup() =
+        benchmarkRule.measureStartup(
+            compilationMode = CompilationMode.DEFAULT,
+            startupMode = StartupMode.COLD,
+            iterations = 1,
+            packageName = "androidx.compose.integration.macrobenchmark.target",
+        ) {
+            action = "androidx.compose.integration.macrobenchmark.target.INSIGHT_ACTIVITY"
+        }
+}
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index 30e582d..f2bb0f2 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -259,6 +259,15 @@
     property public abstract Role paneRole;
   }
 
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldParentData {
+    method public float getMinTouchTargetSize();
+    method public float getPreferredWidth();
+    method public boolean isAnimatedPane();
+    property public abstract boolean isAnimatedPane;
+    property public abstract float minTouchTargetSize;
+    property public abstract float preferredWidth;
+  }
+
   public sealed interface PaneScaffoldScope {
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public androidx.compose.ui.Modifier paneExpansionDraggable(androidx.compose.ui.Modifier, androidx.compose.material3.adaptive.layout.PaneExpansionState state, float minTouchTargetSize, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
     method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
@@ -334,6 +343,10 @@
     property public int size;
   }
 
+  public final class ThreePaneScaffoldHorizontalOrderKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder toLtrOrder(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+  }
+
   public final class ThreePaneScaffoldKt {
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride> getLocalThreePaneScaffoldOverride();
     property @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride> LocalThreePaneScaffoldOverride;
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index 30e582d..f2bb0f2 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -259,6 +259,15 @@
     property public abstract Role paneRole;
   }
 
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldParentData {
+    method public float getMinTouchTargetSize();
+    method public float getPreferredWidth();
+    method public boolean isAnimatedPane();
+    property public abstract boolean isAnimatedPane;
+    property public abstract float minTouchTargetSize;
+    property public abstract float preferredWidth;
+  }
+
   public sealed interface PaneScaffoldScope {
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public androidx.compose.ui.Modifier paneExpansionDraggable(androidx.compose.ui.Modifier, androidx.compose.material3.adaptive.layout.PaneExpansionState state, float minTouchTargetSize, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
     method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
@@ -334,6 +343,10 @@
     property public int size;
   }
 
+  public final class ThreePaneScaffoldHorizontalOrderKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder toLtrOrder(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+  }
+
   public final class ThreePaneScaffoldKt {
     method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride> getLocalThreePaneScaffoldOverride();
     property @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride> LocalThreePaneScaffoldOverride;
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDraggableModifier.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDraggableModifier.kt
index 51623a1..1881c29 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDraggableModifier.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDraggableModifier.kt
@@ -48,7 +48,7 @@
 
 internal class MinTouchTargetSizeNode(var size: Dp) : ParentDataModifierNode, Modifier.Node() {
     override fun Density.modifyParentData(parentData: Any?) =
-        ((parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData()).also {
+        ((parentData as? PaneScaffoldParentDataImpl) ?: PaneScaffoldParentDataImpl()).also {
             it.minTouchTargetSize = size
         }
 }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt
index c6e6f11..cdc8ef9 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt
@@ -104,7 +104,7 @@
 private class PaneMarginsNode(var paneMargins: PaneMargins) :
     ParentDataModifierNode, Modifier.Node() {
     override fun Density.modifyParentData(parentData: Any?) =
-        ((parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData()).also {
+        ((parentData as? PaneScaffoldParentDataImpl) ?: PaneScaffoldParentDataImpl()).also {
             it.paneMargins = paneMargins
         }
 }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index cb6e5f0f..be3751a 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -194,7 +194,7 @@
 
 private class PreferredWidthNode(var width: Dp) : ParentDataModifierNode, Modifier.Node() {
     override fun Density.modifyParentData(parentData: Any?) =
-        ((parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData()).also {
+        ((parentData as? PaneScaffoldParentDataImpl) ?: PaneScaffoldParentDataImpl()).also {
             it.preferredWidth = with(this) { width.toPx() }
         }
 }
@@ -230,11 +230,12 @@
 
 private class AnimatedPaneNode : ParentDataModifierNode, Modifier.Node() {
     override fun Density.modifyParentData(parentData: Any?) =
-        ((parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData()).also {
+        ((parentData as? PaneScaffoldParentDataImpl) ?: PaneScaffoldParentDataImpl()).also {
             it.isAnimatedPane = true
         }
 }
 
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
 internal val List<Measurable>.minTouchTargetSize: Dp
     get() =
         fastMaxOfOrNull {
@@ -247,9 +248,33 @@
             }
         } ?: 0.dp
 
-internal data class PaneScaffoldParentData(
-    var preferredWidth: Float? = null,
+/**
+ * The parent data passed to pane scaffolds by their contents like panes and drag handles.
+ *
+ * @see PaneScaffoldScope.preferredWidth
+ */
+@ExperimentalMaterial3AdaptiveApi
+sealed interface PaneScaffoldParentData {
+    /**
+     * The preferred width of the child, which is supposed to be set via
+     * [PaneScaffoldScope.preferredWidth] on a pane composable, like [AnimatedPane].
+     */
+    val preferredWidth: Float
+
+    /** `true` to indicate that the child is an [AnimatedPane]; otherwise `false`. */
+    val isAnimatedPane: Boolean
+
+    /**
+     * The minimum touch target size of the child, which is supposed to be set via
+     * [PaneScaffoldScope.paneExpansionDraggable] on a drag handle component.
+     */
+    val minTouchTargetSize: Dp
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+internal data class PaneScaffoldParentDataImpl(
+    override var preferredWidth: Float = Float.NaN,
     var paneMargins: PaneMargins = PaneMargins.Unspecified,
-    var isAnimatedPane: Boolean = false,
-    var minTouchTargetSize: Dp = Dp.Unspecified
-)
+    override var isAnimatedPane: Boolean = false,
+    override var minTouchTargetSize: Dp = Dp.Unspecified
+) : PaneScaffoldParentData
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index a861892..1400410 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -777,6 +777,7 @@
     }
 }
 
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private class PaneMeasurable(
     val measurable: Measurable,
     val priority: Int,
@@ -784,16 +785,17 @@
     defaultPreferredWidth: Int
 ) {
     private val data =
-        ((measurable.parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData())
+        ((measurable.parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentDataImpl())
 
     var measuringWidth =
-        if (data.preferredWidth == null || data.preferredWidth!!.isNaN()) {
+        if (data.preferredWidth.isNaN()) {
             defaultPreferredWidth
         } else {
-            data.preferredWidth!!.toInt()
+            data.preferredWidth.toInt()
         }
 
-    val margins: PaneMargins = data.paneMargins
+    // TODO(conradchen): uncomment it when we can expose PaneMargins
+    // val margins: PaneMargins = data.paneMargins
 
     val isAnimatedPane = data.isAnimatedPane
 
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldHorizontalOrder.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldHorizontalOrder.kt
index f24f7fa..c49ce79 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldHorizontalOrder.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldHorizontalOrder.kt
@@ -89,9 +89,13 @@
     }
 }
 
-/** Converts a bidirectional order to a left-to-right order. */
+/**
+ * Converts a bidirectional order to a left-to-right order.
+ *
+ * @param layoutDirection the current [LayoutDirection]
+ */
 @ExperimentalMaterial3AdaptiveApi
-internal fun ThreePaneScaffoldHorizontalOrder.toLtrOrder(
+fun ThreePaneScaffoldHorizontalOrder.toLtrOrder(
     layoutDirection: LayoutDirection
 ): ThreePaneScaffoldHorizontalOrder {
     return if (layoutDirection == LayoutDirection.Rtl) {
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index c20e335..e09c09f 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -1138,6 +1138,10 @@
     property public abstract boolean isEmpty;
   }
 
+  public final class CompositionDataKt {
+    method public static androidx.compose.runtime.tooling.CompositionInstance? findCompositionInstance(androidx.compose.runtime.tooling.CompositionData);
+  }
+
   @kotlin.jvm.JvmDefaultWithCompatibility public interface CompositionGroup extends androidx.compose.runtime.tooling.CompositionData {
     method public Iterable<java.lang.Object?> getData();
     method public default int getGroupSize();
@@ -1155,6 +1159,14 @@
     property public abstract String? sourceInfo;
   }
 
+  public interface CompositionInstance {
+    method public androidx.compose.runtime.tooling.CompositionGroup? findContextGroup();
+    method public androidx.compose.runtime.tooling.CompositionData getData();
+    method public androidx.compose.runtime.tooling.CompositionInstance? getParent();
+    property public abstract androidx.compose.runtime.tooling.CompositionData data;
+    property public abstract androidx.compose.runtime.tooling.CompositionInstance? parent;
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.ExperimentalComposeRuntimeApi public interface CompositionObserver {
     method public void onBeginComposition(androidx.compose.runtime.Composition composition, java.util.Map<androidx.compose.runtime.RecomposeScope,? extends java.util.Set<?>> invalidationMap);
     method public void onEndComposition(androidx.compose.runtime.Composition composition);
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index a5104eee..4c7407c 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -1210,6 +1210,10 @@
     property public abstract boolean isEmpty;
   }
 
+  public final class CompositionDataKt {
+    method public static androidx.compose.runtime.tooling.CompositionInstance? findCompositionInstance(androidx.compose.runtime.tooling.CompositionData);
+  }
+
   @kotlin.jvm.JvmDefaultWithCompatibility public interface CompositionGroup extends androidx.compose.runtime.tooling.CompositionData {
     method public Iterable<java.lang.Object?> getData();
     method public default int getGroupSize();
@@ -1227,6 +1231,14 @@
     property public abstract String? sourceInfo;
   }
 
+  public interface CompositionInstance {
+    method public androidx.compose.runtime.tooling.CompositionGroup? findContextGroup();
+    method public androidx.compose.runtime.tooling.CompositionData getData();
+    method public androidx.compose.runtime.tooling.CompositionInstance? getParent();
+    property public abstract androidx.compose.runtime.tooling.CompositionData data;
+    property public abstract androidx.compose.runtime.tooling.CompositionInstance? parent;
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.ExperimentalComposeRuntimeApi public interface CompositionObserver {
     method public void onBeginComposition(androidx.compose.runtime.Composition composition, java.util.Map<androidx.compose.runtime.RecomposeScope,? extends java.util.Set<?>> invalidationMap);
     method public void onEndComposition(androidx.compose.runtime.Composition composition);
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index abd9693..da4f65e 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -32,6 +32,7 @@
 import androidx.collection.mutableScatterMapOf
 import androidx.collection.mutableScatterSetOf
 import androidx.compose.runtime.Composer.Companion.equals
+import androidx.compose.runtime.ComposerImpl.CompositionContextHolder
 import androidx.compose.runtime.changelist.ChangeList
 import androidx.compose.runtime.changelist.ComposerChangeListWriter
 import androidx.compose.runtime.changelist.FixupList
@@ -48,6 +49,8 @@
 import androidx.compose.runtime.snapshots.fastMap
 import androidx.compose.runtime.snapshots.fastToSet
 import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
+import androidx.compose.runtime.tooling.CompositionInstance
 import androidx.compose.runtime.tooling.LocalInspectionTables
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
@@ -1652,7 +1655,7 @@
         }
 
         parentProvider.read(LocalInspectionTables)?.let {
-            it.add(slotTable)
+            it.add(compositionData)
             parentContext.recordInspectionTable(it)
         }
         startGroup(parentContext.compoundHashKey)
@@ -2187,8 +2190,18 @@
             } else null
         }
 
+    private var _compositionData: CompositionData? = null
+
     override val compositionData: CompositionData
-        get() = slotTable
+        get() {
+            val data = _compositionData
+            if (data == null) {
+                val newData = CompositionDataImpl(composition)
+                _compositionData = newData
+                return newData
+            }
+            return data
+        }
 
     /** Schedule a side effect to run when we apply composition changes. */
     override fun recordSideEffect(effect: () -> Unit) {
@@ -3944,8 +3957,9 @@
      * A holder that will dispose of its [CompositionContext] when it leaves the composition that
      * will not have its reference made visible to user code.
      */
-    private class CompositionContextHolder(val ref: ComposerImpl.CompositionContextImpl) :
+    internal class CompositionContextHolder(val ref: ComposerImpl.CompositionContextImpl) :
         ReusableRememberObserver {
+
         override fun onRemembered() {}
 
         override fun onAbandoned() {
@@ -3958,7 +3972,7 @@
     }
 
     @OptIn(ExperimentalComposeRuntimeApi::class)
-    private inner class CompositionContextImpl(
+    internal inner class CompositionContextImpl(
         override val compoundHashKey: Int,
         override val collectingParameterInformation: Boolean,
         override val collectingSourceInformation: Boolean,
@@ -4010,7 +4024,7 @@
         @OptIn(ExperimentalComposeApi::class)
         @get:OptIn(ExperimentalComposeApi::class)
         override val recomposeCoroutineContext: CoroutineContext
-            get() = composition.recomposeCoroutineContext
+            get() = [email protected]
 
         override fun composeInitial(
             composition: ControlledComposition,
@@ -4105,6 +4119,9 @@
         override fun reportRemovedComposition(composition: ControlledComposition) {
             parentContext.reportRemovedComposition(composition)
         }
+
+        override val composition: Composition
+            get() = [email protected]
     }
 
     private inline fun updateCompoundKeyWhenWeEnterGroup(
@@ -4804,3 +4821,69 @@
     }
     return state
 }
+
+internal class CompositionDataImpl(val composition: Composition) :
+    CompositionData, CompositionInstance {
+    private val slotTable
+        get() = (composition as CompositionImpl).slotTable
+
+    override val compositionGroups: Iterable<CompositionGroup>
+        get() = slotTable.compositionGroups
+
+    override val isEmpty: Boolean
+        get() = slotTable.isEmpty
+
+    override fun find(identityToFind: Any): CompositionGroup? = slotTable.find(identityToFind)
+
+    override fun hashCode(): Int = composition.hashCode() * 31
+
+    override fun equals(other: Any?): Boolean =
+        other is CompositionDataImpl && composition == other.composition
+
+    override val parent: CompositionInstance?
+        get() = composition.parent?.let { CompositionDataImpl(it) }
+
+    override val data: CompositionData
+        get() = this
+
+    override fun findContextGroup(): CompositionGroup? {
+        val parentSlotTable = composition.parent?.slotTable ?: return null
+        val context = composition.context
+
+        parentSlotTable.read { reader ->
+            fun scanGroup(group: Int, end: Int): CompositionGroup? {
+                var current = group
+                while (current < end) {
+                    val next = current + reader.groupSize(current)
+                    if (
+                        reader.hasMark(current) &&
+                            reader.groupKey(current) == referenceKey &&
+                            reader.groupObjectKey(current) == reference
+                    ) {
+                        val contextHolder = reader.groupGet(current, 0) as? CompositionContextHolder
+                        if (contextHolder != null && contextHolder.ref == context) {
+                            return parentSlotTable.compositionGroupOf(current)
+                        }
+                    }
+                    if (reader.containsMark(current)) {
+                        scanGroup(current + 1, next)?.let {
+                            return it
+                        }
+                    }
+                    current = next
+                }
+                return null
+            }
+            return scanGroup(0, reader.size)
+        }
+    }
+
+    private val Composition.slotTable
+        get() = (this as? CompositionImpl)?.slotTable
+
+    private val Composition.context
+        get() = (this as? CompositionImpl)?.parent
+
+    private val Composition.parent
+        get() = context?.composition
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
index 25b9431..6ccb274 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
@@ -103,4 +103,6 @@
     ): MovableContentState? = null
 
     internal abstract fun reportRemovedComposition(composition: ControlledComposition)
+
+    internal abstract val composition: Composition?
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 0926fe9..9a382bd 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -1595,6 +1595,9 @@
     ): MovableContentState? =
         synchronized(stateLock) { movableContentStatesAvailable.remove(reference) }
 
+    override val composition: Composition?
+        get() = null
+
     /**
      * hack: the companion object is thread local in Kotlin/Native to avoid freezing
      * [_runningRecomposers] with the current memory model. As a side effect, recomposers are now
@@ -1617,6 +1620,10 @@
         val runningRecomposers: StateFlow<Set<RecomposerInfo>>
             get() = _runningRecomposers
 
+        internal fun currentRunningRecomposers(): Set<RecomposerInfo> {
+            return _runningRecomposers.value
+        }
+
         internal fun setHotReloadEnabled(value: Boolean) {
             _hotReloadEnabled.set(value)
         }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index cdfa183..68b5f16 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -3218,6 +3218,10 @@
         .replace("MutableState", "σ")
         .let { it.substring(0, min(size, it.length)) }
 
+internal fun SlotTable.compositionGroupOf(group: Int): CompositionGroup {
+    return SlotTableGroup(this, group, this.version)
+}
+
 private class SlotTableGroup(
     val table: SlotTable,
     val group: Int,
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
index 290fe38..4ea8db0 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
@@ -16,7 +16,9 @@
 
 package androidx.compose.runtime.tooling
 
+import androidx.compose.runtime.CompositionContext
 import androidx.compose.runtime.internal.JvmDefaultWithCompatibility
+import androidx.compose.runtime.rememberCompositionContext
 
 /**
  * A [CompositionData] is the data tracked by the composer during composition.
@@ -98,3 +100,35 @@
     val slotsSize: Int
         get() = 0
 }
+
+/**
+ * [CompositionInstance] provides information about the composition of which a [CompositionData] is
+ * part.
+ */
+interface CompositionInstance {
+    /**
+     * The parent composition instance, if the instance is part of a sub-composition. If this is the
+     * root of a composition (such as the content of a ComposeView), then [parent] will be `null`.
+     */
+    val parent: CompositionInstance?
+
+    /** The [CompositionData] for the instance */
+    val data: CompositionData
+
+    /**
+     * Find the [CompositionGroup] that contains the [CompositionContext] created by a call to
+     * [rememberCompositionContext] that is the parent context for this composition. If this is the
+     * root of the composition (e.g. [parent] is `null`) then this method also returns `null`.
+     */
+    fun findContextGroup(): CompositionGroup?
+}
+
+/**
+ * Find the [CompositionInstance] associated with the root [CompositionData]. This is only valid for
+ * instances of [CompositionData] that are recorded in a [LocalInspectionTables] table directly.
+ *
+ * Even though [CompositionGroup]s implement the [CompositionData] interface, only the root
+ * [CompositionData] has an associated [CompositionInstance]. All [CompositionGroup] instances will
+ * return `null`.
+ */
+fun CompositionData.findCompositionInstance(): CompositionInstance? = this as? CompositionInstance
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/tooling/CompositionInstanceTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/tooling/CompositionInstanceTests.kt
new file mode 100644
index 0000000..53b2a8b
--- /dev/null
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/tooling/CompositionInstanceTests.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.tooling
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mock.Text
+import androidx.compose.runtime.mock.View
+import androidx.compose.runtime.mock.ViewApplier
+import androidx.compose.runtime.mock.compositionTest
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.runtime.rememberUpdatedState
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+class CompositionInstanceTests {
+    @Test
+    fun canFindACompositionInstance() = compositionTest {
+        val table = mutableSetOf<CompositionData>()
+        compose {
+            CompositionLocalProvider(LocalInspectionTables provides table) {
+                TestSubcomposition { Text("Some value") }
+            }
+        }
+
+        assertEquals(1, table.size)
+        val data = table.first()
+        val instance = data.findCompositionInstance()
+        assertNotNull(instance)
+    }
+
+    @Test
+    fun canFindContextGroup() = compositionTest {
+        val table = mutableSetOf<CompositionData>()
+        compose {
+            CompositionLocalProvider(LocalInspectionTables provides table) {
+                TestSubcomposition { Text("Some value") }
+            }
+        }
+
+        val data = table.first()
+        val instance = data.findCompositionInstance()
+        assertNotNull(instance)
+        val contextGroup = instance.findContextGroup()
+        assertNotNull(contextGroup)
+    }
+
+    @Test
+    fun canFindParentInstance() = compositionTest {
+        val table = mutableSetOf<CompositionData>()
+        compose {
+            CompositionLocalProvider(LocalInspectionTables provides table) {
+                TestSubcomposition {
+                    TestSubcomposition { TestSubcomposition { Text("Some value") } }
+                }
+            }
+        }
+
+        assertEquals(3, table.size)
+
+        // Find the root (which will not be in the table)
+        fun findRootOf(data: CompositionData): CompositionData {
+            val parentData = data.findCompositionInstance()?.parent?.data
+            return if (parentData == null) data else findRootOf(parentData)
+        }
+
+        val root = findRootOf(table.first())
+
+        // Verify that the instance and its parents (not the root) are in the table
+        fun verify(data: CompositionData) {
+            if (data != root) {
+                assertTrue(data in table)
+                data.findCompositionInstance()?.parent?.let { verify(it.data) }
+            }
+        }
+
+        for (instance in table) {
+            assertEquals(root, findRootOf(instance))
+            verify(instance)
+        }
+    }
+
+    @Test
+    fun canFindParentNotInFirstPosition() = compositionTest {
+        val table = mutableSetOf<CompositionData>()
+        compose {
+            CompositionLocalProvider(LocalInspectionTables provides table) {
+                Text("Some value")
+                Text("Some value")
+                Text("Some value")
+                TestSubcomposition { Text("Some value") }
+            }
+        }
+        val instance = table.first().findCompositionInstance()
+        assertNotNull(instance)
+        val contextGroup = instance.findContextGroup()
+        assertNotNull(contextGroup)
+    }
+
+    @Test
+    fun contextGroupIsInParent() = compositionTest {
+        val table = mutableSetOf<CompositionData>()
+        compose {
+            CompositionLocalProvider(LocalInspectionTables provides table) {
+                TestSubcomposition { Text("Some value") }
+            }
+        }
+        val instance = table.first().findCompositionInstance()
+        assertNotNull(instance)
+        val contextGroup = instance.findContextGroup()
+        assertNotNull(contextGroup)
+        val parentData = instance.parent?.data
+        assertNotNull(parentData)
+        val identity = contextGroup.identity
+        assertNotNull(identity)
+        val foundGroup = parentData.find(identity)
+        assertNotNull(foundGroup)
+        assertEquals(identity, foundGroup.identity)
+
+        fun identityMap(data: CompositionData): Map<Any, CompositionGroup> {
+            val result = mutableMapOf<Any, CompositionGroup>()
+            fun addToMap(group: CompositionGroup) {
+                val groupIdentity = group.identity
+                if (groupIdentity != null) result[groupIdentity] = group
+                group.compositionGroups.forEach(::addToMap)
+            }
+            data.compositionGroups.forEach(::addToMap)
+            return result
+        }
+
+        val map = identityMap(parentData)
+        val mapFoundGroup = map[contextGroup.identity]
+        assertNotNull(mapFoundGroup)
+    }
+}
+
+@Composable
+internal fun TestSubcomposition(content: @Composable () -> Unit) {
+    val parentRef = rememberCompositionContext()
+    val currentContent by rememberUpdatedState(content)
+    DisposableEffect(parentRef) {
+        val subComposeRoot = View().apply { name = "subComposeRoot" }
+        val subcomposition = Composition(ViewApplier(subComposeRoot), parentRef)
+        // TODO: work around for b/179701728
+        callSetContent(subcomposition) {
+            // Note: This is in a lambda invocation to keep the currentContent state read
+            // in the sub-composition's content composable. Changing this to be
+            // subcomposition.setContent(currentContent) would snapshot read only on initial set.
+            currentContent()
+        }
+        onDispose { subcomposition.dispose() }
+    }
+}
+
+private fun callSetContent(composition: Composition, content: @Composable () -> Unit) {
+    composition.setContent(content)
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
index 939dfd0..3e46712 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
@@ -21,6 +21,7 @@
 import android.view.ViewStructure
 import android.view.autofill.AutofillValue
 import androidx.autofill.HintConstants.AUTOFILL_HINT_PERSON_NAME
+import androidx.compose.ui.ComposeUiFlags.isSemanticAutofillEnabled
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.platform.LocalAutofill
@@ -76,6 +77,7 @@
     @SdkSuppress(minSdkVersion = 26)
     @Test
     fun onProvideAutofillVirtualStructure_populatesViewStructure() {
+        if (isSemanticAutofillEnabled) return
         // Arrange.
         val viewStructure: ViewStructure = FakeViewStructure()
         val autofillNode =
@@ -110,6 +112,7 @@
     @SdkSuppress(minSdkVersion = 26)
     @Test
     fun autofill_triggersOnFill() {
+        if (isSemanticAutofillEnabled) return
         // Arrange.
         val expectedValue = "PersonName"
         var autofilledValue = ""
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 2546cb2..7f80faf 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -187,8 +187,8 @@
 
     private fun notifyAutofillValueChanged(semanticsId: Int, newAutofillValue: Any) {
         val currSemanticsNode = currentSemanticsNodes[semanticsId]?.semanticsNode
-        val currDataType = currSemanticsNode?.unmergedConfig?.getOrNull(SemanticsContentDataType)
-
+        val currDataType =
+            currSemanticsNode?.unmergedConfig?.getOrNull(SemanticsContentDataType) ?: return
         when (currDataType) {
             ContentDataType.Text ->
                 autofillManager.notifyValueChanged(
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
index 2756300..880b9a2 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
@@ -21,6 +21,12 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.graphics.Rect;
@@ -32,6 +38,8 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import androidx.core.view.ScrollFeedbackProviderCompat;
+import androidx.core.view.ViewCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -45,6 +53,7 @@
 
     private NestedScrollView mNestedScrollView;
     private View mChild;
+    private ScrollFeedbackProviderCompat mScrollFeedbackProvider;
 
     @Test
     public void getBottomFadingEdgeStrength_childBottomIsBelowParentWithoutMargins_isCorrect() {
@@ -456,6 +465,78 @@
         assertEquals(EdgeEffectSubstitute.State.Idle, edgeEffect.getState());
     }
 
+    @Test
+    public void scrollFeedbackCallbacks() {
+        setup(200);
+        mScrollFeedbackProvider = mock(ScrollFeedbackProviderCompat.class);
+        mNestedScrollView.mScrollFeedbackProvider = mScrollFeedbackProvider;
+        setChildMargins(0, 0);
+        measureAndLayout(100);
+
+        MotionEvent ev = createScrollMotionEvent(
+                /* scrollAmount= */ 2f,
+                /* deviceId= */ 3,
+                /* source= */ InputDevice.SOURCE_ROTARY_ENCODER,
+                /* axis= */ MotionEvent.AXIS_SCROLL);
+        // Scroll up by -2.
+        mNestedScrollView.scrollBy(
+                /* verticalScrollDistance= */ -2, MotionEvent.AXIS_SCROLL, ev, /* x= */ 3,
+                ViewCompat.TYPE_TOUCH, /* isSourceMouseOrKeyboard= */ false);
+
+        // Since the view was already at the very top, a scroll up by -2 should not cause a
+        // onScrollProgress call, but should call onScrollLimit.
+        verify(mScrollFeedbackProvider, never()).onScrollProgress(
+                anyInt(), anyInt(), anyInt(), anyInt());
+        verify(mScrollFeedbackProvider).onScrollLimit(
+                /* inputDeviceId= */ 3, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ true);
+        reset(mScrollFeedbackProvider);
+
+        // Scroll down by 20.
+        mNestedScrollView.scrollBy(
+                /* verticalScrollDistance= */ 20, MotionEvent.AXIS_SCROLL, ev, /* x= */ 3,
+                ViewCompat.TYPE_TOUCH, /* isSourceMouseOrKeyboard= */ false);
+
+        // The height of the view is 100. Since the scroll is 20 pixels, we expect all of it to be
+        // consumed. So, expect onScrollProgress with 20 pixels, and no onScrollLimit.
+        verify(mScrollFeedbackProvider).onScrollProgress(
+                /* inputDeviceId= */ 3, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 20);
+        verify(mScrollFeedbackProvider, never()).onScrollLimit(
+                anyInt(), anyInt(), anyInt(), anyBoolean());
+
+        // At this point, the view was at y=20. So a scroll of 100 pixels should do a consumed
+        // scroll of 80 pixels, and also cause an onScrollLimit call.
+        mNestedScrollView.scrollBy(
+                /* verticalScrollDistance= */ 100, MotionEvent.AXIS_SCROLL, ev, /* x= */ 3,
+                ViewCompat.TYPE_TOUCH, /* isSourceMouseOrKeyboard= */ false);
+
+        verify(mScrollFeedbackProvider).onScrollProgress(
+                /* inputDeviceId= */ 3, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 80);
+        verify(mScrollFeedbackProvider).onScrollLimit(
+                /* inputDeviceId= */ 3, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+    }
+
+    @Test
+    public void scrollFeedbackCallbacks_motionEventUnavailable() {
+        setup(200);
+        mScrollFeedbackProvider = mock(ScrollFeedbackProviderCompat.class);
+        mNestedScrollView.mScrollFeedbackProvider = mScrollFeedbackProvider;
+        setChildMargins(0, 0);
+        measureAndLayout(100);
+
+        mNestedScrollView.scrollBy(
+                /* verticalScrollDistance= */ -2, MotionEvent.AXIS_SCROLL, /* ev= */ null,
+                /* x= */ 3,  ViewCompat.TYPE_TOUCH, /* isSourceMouseOrKeyboard= */ false);
+
+        verify(mScrollFeedbackProvider, never()).onScrollProgress(
+                anyInt(), anyInt(), anyInt(), anyInt());
+        verify(mScrollFeedbackProvider, never()).onScrollLimit(
+                anyInt(), anyInt(), anyInt(), anyBoolean());
+    }
+
     private void swipeDown(boolean shortSwipe) {
         float endY = shortSwipe ? mNestedScrollView.getHeight() / 2f :
                 mNestedScrollView.getHeight() - 1;
@@ -495,7 +576,8 @@
         mNestedScrollView.dispatchTouchEvent(up);
     }
 
-    private void sendScroll(float scrollAmount, int source) {
+    private MotionEvent createScrollMotionEvent(
+            float scrollAmount, int deviceId, int source, int axis) {
         float x = mNestedScrollView.getWidth() / 2f;
         float y = mNestedScrollView.getHeight() / 2f;
         MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties();
@@ -503,11 +585,10 @@
         MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords();
         pointerCoords.x = x;
         pointerCoords.y = y;
-        int axis = source == InputDevice.SOURCE_ROTARY_ENCODER ? MotionEvent.AXIS_SCROLL
-                : MotionEvent.AXIS_VSCROLL;
+
         pointerCoords.setAxisValue(axis, scrollAmount);
 
-        MotionEvent scroll = MotionEvent.obtain(
+        return MotionEvent.obtain(
                 0, /* downTime */
                 0, /* eventTime */
                 MotionEvent.ACTION_SCROLL, /* action */
@@ -518,13 +599,18 @@
                 0, /* buttonState */
                 0f, /* xPrecision */
                 0f, /* yPrecision */
-                0, /* deviceId */
+                deviceId,
                 0, /* edgeFlags */
                 source, /* source */
                 0 /* flags */
         );
+    }
 
-        mNestedScrollView.dispatchGenericMotionEvent(scroll);
+    private void sendScroll(float scrollAmount, int source) {
+        int axis = source == InputDevice.SOURCE_ROTARY_ENCODER
+                ? MotionEvent.AXIS_SCROLL : MotionEvent.AXIS_VSCROLL;
+        mNestedScrollView.dispatchGenericMotionEvent(
+                createScrollMotionEvent(scrollAmount, /* deviceId= */ 1, source, axis));
     }
 
     private void setup(int childHeight) {
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index cbe5e8c..29e8f48 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -63,6 +63,7 @@
 import androidx.core.view.NestedScrollingChildHelper;
 import androidx.core.view.NestedScrollingParent3;
 import androidx.core.view.NestedScrollingParentHelper;
+import androidx.core.view.ScrollFeedbackProviderCompat;
 import androidx.core.view.ScrollingView;
 import androidx.core.view.ViewCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
@@ -136,6 +137,10 @@
     @NonNull
     public EdgeEffect mEdgeGlowBottom;
 
+    @VisibleForTesting
+    ScrollFeedbackProviderCompat mScrollFeedbackProvider =
+            ScrollFeedbackProviderCompat.createProvider(this);
+
     /**
      * Position of the last motion event; only used with touch related events (usually to assist
      * in movement changes in a drag gesture).
@@ -973,7 +978,9 @@
 
                 if (mIsBeingDragged) {
                     final int x = (int) motionEvent.getX(activePointerIndex);
-                    int scrollOffset = scrollBy(deltaY, x, ViewCompat.TYPE_TOUCH, false);
+                    int scrollOffset =
+                            scrollBy(deltaY, MotionEvent.AXIS_Y, motionEvent, x,
+                                    ViewCompat.TYPE_TOUCH, false);
                     // Updates the global positions (used by later move events to properly scroll).
                     mLastMotionY = y - scrollOffset;
                     mNestedYOffset += scrollOffset;
@@ -1052,7 +1059,24 @@
         mEdgeGlowBottom.onRelease();
     }
 
-    /*
+    /**
+     * Same as {@link #scrollBy(int, int, MotionEvent, int, int, boolean)}, but with no entry for
+     * the vertical motion axis as well as the {@link MotionEvent}.
+     *
+     * <p>Use this method (instead of the other overload) if the {@link MotionEvent} that caused
+     * this scroll request is not known.
+     */
+    private int scrollBy(
+            int verticalScrollDistance,
+            int x,
+            int touchType,
+            boolean isSourceMouseOrKeyboard
+    ) {
+        return scrollBy(verticalScrollDistance, /* verticalScrollAxis= */ -1, null, x, touchType,
+                isSourceMouseOrKeyboard);
+    }
+
+    /**
      * Handles scroll events for both touch and non-touch events (mouse scroll wheel,
      * rotary button, keyboard, etc.).
      *
@@ -1060,12 +1084,28 @@
      * for calculating the total scroll between multiple move events (touch). This returned value
      * is NOT needed for non-touch events since a scroll is a one time event (vs. touch where a
      * drag may be triggered multiple times with the movement of the finger).
+     *
+     * @param verticalScrollDistance the amount of distance (in pixels) to scroll vertically.
+     * @param verticalScrollAxis the motion axis that triggered the vertical scroll. This is not
+     *                           always {@link MotionEvent#AXIS_Y}, because there could be other
+     *                           axes that trigger a vertical scroll on the view. For example,
+     *                           generic motion events reported via {@link MotionEvent#AXIS_SCROLL}
+     *                           or {@link MotionEvent#AXIS_VSCROLL}. Use {@code -1} if the vertical
+     *                           scroll axis is not known.
+     * @param ev the {@link MotionEvent} that caused this scroll. {@code null} if the event is not
+     *           known.
+     * @param x the target location on the x axis.
+     * @param touchType the {@link ViewCompat.NestedScrollType} for this scroll.
+     * @param isSourceMouseOrKeyboard whether or not the scroll was caused by a mouse or a keyboard.
      */
     // TODO: You should rename this to nestedScrollBy() so it is different from View.scrollBy
-    private int scrollBy(
+    @VisibleForTesting
+    int scrollBy(
             int verticalScrollDistance,
+            int verticalScrollAxis,
+            @Nullable MotionEvent ev,
             int x,
-            int touchType,
+            @ViewCompat.NestedScrollType int touchType,
             boolean isSourceMouseOrKeyboard
     ) {
         int totalScrollOffset = 0;
@@ -1121,6 +1161,10 @@
 
         // The position may have been adjusted in the previous call, so we must revise our values.
         final int scrollYDelta = getScrollY() - initialScrollY;
+        if (ev != null && scrollYDelta != 0) {
+            mScrollFeedbackProvider.onScrollProgress(
+                    ev.getDeviceId(),  ev.getSource(), verticalScrollAxis, scrollYDelta);
+        }
         final int unconsumedY = verticalScrollDistance - scrollYDelta;
 
         // Reset the Y consumed scroll to zero
@@ -1150,6 +1194,11 @@
                         (float) -verticalScrollDistance / getHeight(),
                         (float) x / getWidth()
                 );
+                if (ev != null) {
+                    mScrollFeedbackProvider.onScrollLimit(
+                            ev.getDeviceId(), ev.getSource(), verticalScrollAxis,
+                            /* isStart= */ true);
+                }
 
                 if (!mEdgeGlowBottom.isFinished()) {
                     mEdgeGlowBottom.onRelease();
@@ -1163,6 +1212,11 @@
                         (float) verticalScrollDistance / getHeight(),
                         1.f - ((float) x / getWidth())
                 );
+                if (ev != null) {
+                    mScrollFeedbackProvider.onScrollLimit(
+                            ev.getDeviceId(), ev.getSource(), verticalScrollAxis,
+                            /* isStart= */ false);
+                }
 
                 if (!mEdgeGlowTop.isFinished()) {
                     mEdgeGlowTop.onRelease();
@@ -1328,12 +1382,12 @@
         if (motionEvent.getAction() == MotionEvent.ACTION_SCROLL && !mIsBeingDragged) {
             final float verticalScroll;
             final int x;
-            final int flingAxis;
+            final int axis;
 
             if (MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_CLASS_POINTER)) {
                 verticalScroll = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL);
                 x = (int) motionEvent.getX();
-                flingAxis = MotionEvent.AXIS_VSCROLL;
+                axis = MotionEvent.AXIS_VSCROLL;
             } else if (
                     MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_ROTARY_ENCODER)
             ) {
@@ -1341,11 +1395,11 @@
                 // Since a Wear rotary event doesn't have a true X and we want to support proper
                 // overscroll animations, we put the x at the center of the screen.
                 x = getWidth() / 2;
-                flingAxis = MotionEvent.AXIS_SCROLL;
+                axis = MotionEvent.AXIS_SCROLL;
             } else {
                 verticalScroll = 0;
                 x = 0;
-                flingAxis = 0;
+                axis = 0;
             }
 
             if (verticalScroll != 0) {
@@ -1355,9 +1409,10 @@
                 final boolean isSourceMouse =
                         MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_MOUSE);
 
-                scrollBy(-invertedDelta, x, ViewCompat.TYPE_NON_TOUCH, isSourceMouse);
-                if (flingAxis != 0) {
-                    mDifferentialMotionFlingController.onMotionEvent(motionEvent, flingAxis);
+                scrollBy(-invertedDelta, axis, motionEvent, x, ViewCompat.TYPE_NON_TOUCH,
+                        isSourceMouse);
+                if (axis != 0) {
+                    mDifferentialMotionFlingController.onMotionEvent(motionEvent, axis);
                 }
 
                 return true;
diff --git a/libraryversions.toml b/libraryversions.toml
index e0fec0e..7139a8e 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -7,7 +7,7 @@
 ARCH_CORE = "2.3.0-alpha01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
 AUTOFILL = "1.3.0-rc01"
-BENCHMARK = "1.4.0-alpha05"
+BENCHMARK = "1.4.0-alpha06"
 BIOMETRIC = "1.4.0-alpha02"
 BLUETOOTH = "1.0.0-alpha02"
 BROWSER = "1.9.0-alpha01"
@@ -116,7 +116,7 @@
 PRIVACYSANDBOX_UI = "1.0.0-alpha11"
 PROFILEINSTALLER = "1.5.0-alpha01"
 RECOMMENDATION = "1.1.0-alpha01"
-RECYCLERVIEW = "1.4.0-rc01"
+RECYCLERVIEW = "1.5.0-alpha01"
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
 REMOTECALLBACK = "1.0.0-alpha02"
 RESOURCEINSPECTION = "1.1.0-alpha01"
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index d241850..1ea74b7 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -119,7 +119,7 @@
                 }
 
                 // TODO(b/337793172): Replace with a default fragment
-                switchContentFragment(ResizeFragment(), "Resize Fragment")
+                switchContentFragment(ResizeFragment(), "Resize CUJ")
 
                 setWindowsInsetsListener()
                 initializeOptionsButton()
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index e60f8b5..f9c876e 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -15,7 +15,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.8.1")
-    api("androidx.core:core:1.13.0")
+    implementation(project(path: ":core:core"))
     implementation("androidx.collection:collection:1.4.2")
     api("androidx.customview:customview:1.0.0")
     implementation("androidx.customview:customview-poolingcontainer:1.0.0")
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewBasicTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewBasicTest.java
index f524970..0ec5d08 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewBasicTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewBasicTest.java
@@ -24,6 +24,12 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
@@ -44,9 +50,14 @@
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
+import androidx.core.util.Pair;
+import androidx.core.view.InputDeviceCompat;
+import androidx.core.view.ScrollFeedbackProviderCompat;
+import androidx.core.view.ViewCompat;
 import androidx.recyclerview.test.R;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
@@ -66,6 +77,8 @@
 
     RecyclerView mRecyclerView;
 
+    ScrollFeedbackProviderCompat mScrollFeedbackProvider;
+
     @Before
     public void setUp() throws Exception {
         mRecyclerView = new RecyclerView(getContext());
@@ -148,6 +161,76 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void scrollFeedbackCallbacks() {
+        mScrollFeedbackProvider = mock(ScrollFeedbackProviderCompat.class);
+        mRecyclerView.mScrollFeedbackProvider = mScrollFeedbackProvider;
+        mRecyclerView.setAdapter(new MockAdapter(20));
+        MockLayoutManager layoutManager = new MockLayoutManager();
+        mRecyclerView.setLayoutManager(layoutManager);
+        measure();
+        layout();
+
+        MotionEvent ev = TouchUtils.createMotionEvent(
+                /* inputDeviceId= */ 1,
+                InputDeviceCompat.SOURCE_TOUCHSCREEN,
+                MotionEvent.ACTION_MOVE,
+                List.of(
+                    Pair.create(MotionEvent.AXIS_X, 10),
+                    Pair.create(MotionEvent.AXIS_Y, -20)));
+        layoutManager.mConsumedHorizontalScroll = 3;
+        layoutManager.mConsumedVerticalScroll = -20;
+        mRecyclerView.scrollByInternal(
+                /* x= */ 10,
+                /* y= */ -20,
+                /* horizontalAxis= */ MotionEvent.AXIS_X,
+                /* verticalAxis= */ MotionEvent.AXIS_Y,
+                ev,
+                ViewCompat.TYPE_TOUCH);
+
+        // Verify onScrollProgress calls equating to the amount of consumed pixels on each axis.
+        verify(mScrollFeedbackProvider).onScrollProgress(
+                /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_X,
+                /* deltaInPixels= */ 3);
+        verify(mScrollFeedbackProvider).onScrollProgress(
+                /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_Y,
+                /* deltaInPixels= */ -20);
+        // Part of the X scroll was not consumed, so expect an onScrollLimit call.
+        verify(mScrollFeedbackProvider).onScrollLimit(
+                /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_X,
+                /* isStart= */ false);
+        // All of the Y scroll was consumed. So expect no onScrollLimit call.
+        verify(mScrollFeedbackProvider, never()).onScrollLimit(
+                /* inputDeviceId= */ anyInt(), anyInt(), eq(MotionEvent.AXIS_Y),
+                /* isStart= */ eq(false));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void scrollFeedbackCallbacks_motionEventUnavailable() {
+        mScrollFeedbackProvider = mock(ScrollFeedbackProviderCompat.class);
+        mRecyclerView.mScrollFeedbackProvider = mScrollFeedbackProvider;
+        mRecyclerView.setAdapter(new MockAdapter(20));
+        MockLayoutManager layoutManager = new MockLayoutManager();
+        mRecyclerView.setLayoutManager(layoutManager);
+        measure();
+        layout();
+
+        mRecyclerView.scrollByInternal(
+                /* x= */ 10,
+                /* y= */ -20,
+                /* horizontalAxis= */ -1,
+                /* verticalAxis= */ -1,
+                null,
+                ViewCompat.TYPE_TOUCH);
+
+        verify(mScrollFeedbackProvider, never()).onScrollProgress(
+                anyInt(), anyInt(), anyInt(), anyInt());
+        verify(mScrollFeedbackProvider, never()).onScrollLimit(
+                anyInt(), anyInt(), anyInt(), anyBoolean());
+    }
+
+    @Test
     public void smoothScrollToPositionWithoutLayoutManager() throws InterruptedException {
         mRecyclerView.setAdapter(new MockAdapter(20));
         measure();
@@ -614,6 +697,9 @@
         int mAdapterChangedCount = 0;
         int mItemsChangedCount = 0;
 
+        int mConsumedHorizontalScroll = Integer.MIN_VALUE;
+        int mConsumedVerticalScroll = Integer.MIN_VALUE;
+
         RecyclerView.Adapter mPrevAdapter;
 
         RecyclerView.Adapter mNextAdapter;
@@ -675,13 +761,13 @@
         @Override
         public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
                 RecyclerView.State state) {
-            return dx;
+            return mConsumedHorizontalScroll != Integer.MIN_VALUE ? mConsumedHorizontalScroll : dx;
         }
 
         @Override
         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
                 RecyclerView.State state) {
-            return dy;
+            return mConsumedVerticalScroll != Integer.MIN_VALUE ? mConsumedVerticalScroll : dy;
         }
 
         @Override
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewOnGenericMotionEventTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewOnGenericMotionEventTest.java
index e164745..f1098a1 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewOnGenericMotionEventTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewOnGenericMotionEventTest.java
@@ -328,10 +328,16 @@
             return super.onGenericMotionEvent(ev);
         }
 
-        boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
+        boolean scrollByInternal(
+                int x,
+                int y,
+                int horizontalAxis,
+                int verticalAxis,
+                @Nullable MotionEvent ev,
+                int type) {
             mTotalX += x;
             mTotalY += y;
-            return super.scrollByInternal(x, y, ev, type);
+            return super.scrollByInternal(x, y, horizontalAxis, verticalAxis, ev, type);
         }
 
         void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator,
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/TouchUtils.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/TouchUtils.java
index 69df81d..83785d8 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/TouchUtils.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/TouchUtils.java
@@ -23,6 +23,10 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 
+import androidx.core.util.Pair;
+
+import java.util.List;
+
 /**
  * RV specific layout tests.
  */
@@ -173,14 +177,30 @@
     /** Creates a {@link MotionEvent} with provided input and motion values. */
     static MotionEvent createMotionEvent(
             int inputDeviceId, int inputSource, int axis, int axisValue) {
+        return createMotionEvent(
+                inputDeviceId, inputSource, MotionEvent.ACTION_SCROLL,
+                List.of(Pair.create(axis, axisValue)));
+    }
+
+    /**
+     * Creates a {@link MotionEvent} with provided input and motion values.
+     *
+     * <p>Allows passing values for multiple axes. Each axis value is represented as a
+     * {@link Pair} of the axis and the respective axis value.
+     */
+    static MotionEvent createMotionEvent(
+            int inputDeviceId, int inputSource, int action,
+            List<Pair<Integer, Integer>> axisValues) {
         MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
         props.id = 0;
         MotionEvent.PointerProperties[] pointerProperties = {props};
         MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
-        coords.setAxisValue(axis, axisValue);
+        for (Pair<Integer, Integer> axisValue : axisValues) {
+            coords.setAxisValue(axisValue.first, axisValue.second);
+        }
         MotionEvent.PointerCoords[] pointerCoords = {coords};
         return MotionEvent.obtain(
-                0, System.currentTimeMillis(), MotionEvent.ACTION_SCROLL,
+                0, System.currentTimeMillis(), action,
                 1, pointerProperties, pointerCoords, 0, 0, 1, 1, inputDeviceId, 0, inputSource, 0);
     }
 
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index 0693836..ab7d486 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -82,6 +82,7 @@
 import androidx.core.view.NestedScrollingChild2;
 import androidx.core.view.NestedScrollingChild3;
 import androidx.core.view.NestedScrollingChildHelper;
+import androidx.core.view.ScrollFeedbackProviderCompat;
 import androidx.core.view.ScrollingView;
 import androidx.core.view.ViewCompat;
 import androidx.core.view.ViewConfigurationCompat;
@@ -768,6 +769,11 @@
     @VisibleForTesting
     DifferentialMotionFlingController mDifferentialMotionFlingController =
             new DifferentialMotionFlingController(getContext(), mDifferentialMotionFlingTarget);
+
+    @VisibleForTesting
+    ScrollFeedbackProviderCompat mScrollFeedbackProvider =
+            ScrollFeedbackProviderCompat.createProvider(this);
+
     public RecyclerView(@NonNull Context context) {
         this(context, null);
     }
@@ -2060,7 +2066,12 @@
         final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
         final boolean canScrollVertical = mLayout.canScrollVertically();
         if (canScrollHorizontal || canScrollVertical) {
-            scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null,
+            scrollByInternal(
+                    canScrollHorizontal ? x : 0,
+                    canScrollVertical ? y : 0,
+                    /* horizontalAxis= */ -1,
+                    /* verticalAxis= */ -1,
+                    /* ev= */ null,
                     TYPE_TOUCH);
         }
     }
@@ -2072,21 +2083,32 @@
      * @see androidx.core.view.NestedScrollingChild
      */
     public void nestedScrollBy(int x, int y) {
-        nestedScrollByInternal(x, y, null, TYPE_NON_TOUCH);
+        nestedScrollByInternal(x, y, -1, -1, null, TYPE_NON_TOUCH);
     }
 
     /**
-     * Similar to {@link RecyclerView#scrollByInternal(int, int, MotionEvent, int)}, but fully
-     * participates in nested scrolling "end to end", meaning that it will start nested scrolling,
-     * participate in nested scrolling, and then end nested scrolling all within one call.
+     * Similar to {@link RecyclerView#scrollByInternal(int, int, int, int, MotionEvent, int)}, but
+     * fully participates in nested scrolling "end to end", meaning that it will start nested
+     * scrolling, participate in nested scrolling, and then end nested scrolling all within one
+     * call.
+     *
      * @param x The amount of horizontal scroll requested.
      * @param y The amount of vertical scroll requested.
+     * @param horizontalAxis the {@link MotionEvent} axis that caused the {@code x} scroll, or -1 if
+     *      not known.
+     * @param verticalAxis the {@link MotionEvent} axis that caused the {@code y} scroll, or -1 if
+     *      not known.
      * @param motionEvent The originating MotionEvent if any.
      * @param type The type of nested scrolling to engage in (TYPE_TOUCH or TYPE_NON_TOUCH).
      */
     @SuppressWarnings("SameParameterValue")
-    private void nestedScrollByInternal(int x, int y, @Nullable MotionEvent motionEvent, int type) {
-
+    private void nestedScrollByInternal(
+            int x,
+            int y,
+            int horizontalAxis,
+            int verticalAxis,
+            @Nullable MotionEvent motionEvent,
+            int type) {
         if (mLayout == null) {
             Log.e(TAG, "Cannot scroll without a LayoutManager set. "
                     + "Call setLayoutManager with a non-null argument.");
@@ -2126,6 +2148,8 @@
         scrollByInternal(
                 canScrollHorizontal ? x : 0,
                 canScrollVertical ? y : 0,
+                horizontalAxis,
+                verticalAxis,
                 motionEvent, type);
         if (mGapWorker != null && (x != 0 || y != 0)) {
             mGapWorker.postFromTraversal(this, x, y);
@@ -2243,11 +2267,21 @@
      *
      * @param x  The amount of horizontal scroll request
      * @param y  The amount of vertical scroll request
-     * @param ev The originating MotionEvent, or null if not from a touch event.
+     * @param horizontalAxis the {@link MotionEvent} axis that caused the {@code x} scroll, or -1 if
+     *      not known.
+     * @param verticalAxis the {@link MotionEvent} axis that caused the {@code y} scroll, or -1 if
+     *      not known.
+     * @param ev The originating MotionEvent, or null if unknown.
      * @param type NestedScrollType, TOUCH or NON_TOUCH.
      * @return Whether any scroll was consumed in either direction.
      */
-    boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
+    boolean scrollByInternal(
+            int x,
+            int y,
+            int horizontalAxis,
+            int verticalAxis,
+            @Nullable MotionEvent ev,
+            int type) {
         int unconsumedX = 0;
         int unconsumedY = 0;
         int consumedX = 0;
@@ -2281,9 +2315,21 @@
         mNestedOffsets[0] += mScrollOffset[0];
         mNestedOffsets[1] += mScrollOffset[1];
 
+        if (ev != null) {
+            if (consumedX != 0) {
+                mScrollFeedbackProvider.onScrollProgress(
+                        ev.getDeviceId(), ev.getSource(), horizontalAxis, consumedX);
+            }
+            if (consumedY != 0) {
+                mScrollFeedbackProvider.onScrollProgress(
+                        ev.getDeviceId(), ev.getSource(), verticalAxis, consumedY);
+            }
+        }
+
         if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
             if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
-                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
+                pullGlows(ev, ev.getX(), horizontalAxis, unconsumedX, ev.getY(), verticalAxis,
+                        unconsumedY);
                 // For rotary encoders, we release stretch EdgeEffects after they are pulled, to
                 // avoid the effects being stuck pulled.
                 if (Build.VERSION.SDK_INT >= 31
@@ -3061,27 +3107,50 @@
     /**
      * Apply a pull to relevant overscroll glow effects
      */
-    private void pullGlows(float x, float overscrollX, float y, float overscrollY) {
+    private void pullGlows(
+            MotionEvent ev,
+            float x,
+            int horizontalAxis,
+            float overscrollX,
+            float y,
+            int verticalAxis,
+            float overscrollY) {
         boolean invalidate = false;
         if (overscrollX < 0) {
             ensureLeftGlow();
             EdgeEffectCompat.onPullDistance(mLeftGlow, -overscrollX / getWidth(),
                     1f - y / getHeight());
+            if (ev != null) {
+                mScrollFeedbackProvider.onScrollLimit(
+                        ev.getDeviceId(), ev.getSource(), horizontalAxis, /* isStart= */ true);
+            }
             invalidate = true;
         } else if (overscrollX > 0) {
             ensureRightGlow();
             EdgeEffectCompat.onPullDistance(mRightGlow, overscrollX / getWidth(), y / getHeight());
+            if (ev != null) {
+                mScrollFeedbackProvider.onScrollLimit(
+                        ev.getDeviceId(), ev.getSource(), horizontalAxis, /* isStart= */ false);
+            }
             invalidate = true;
         }
 
         if (overscrollY < 0) {
             ensureTopGlow();
             EdgeEffectCompat.onPullDistance(mTopGlow, -overscrollY / getHeight(), x / getWidth());
+            if (ev != null) {
+                mScrollFeedbackProvider.onScrollLimit(
+                        ev.getDeviceId(), ev.getSource(), verticalAxis, /* isStart= */ true);
+            }
             invalidate = true;
         } else if (overscrollY > 0) {
             ensureBottomGlow();
             EdgeEffectCompat.onPullDistance(mBottomGlow, overscrollY / getHeight(),
                     1f - x / getWidth());
+            if (ev != null) {
+                mScrollFeedbackProvider.onScrollLimit(
+                        ev.getDeviceId(), ev.getSource(), verticalAxis, /* isStart= */ false);
+            }
             invalidate = true;
         }
 
@@ -3969,6 +4038,8 @@
                     if (scrollByInternal(
                             canScrollHorizontally ? dx : 0,
                             canScrollVertically ? dy : 0,
+                            MotionEvent.AXIS_X,
+                            MotionEvent.AXIS_Y,
                             e, TYPE_TOUCH)) {
                         getParent().requestDisallowInterceptTouchEvent(true);
                     }
@@ -4047,6 +4118,8 @@
         }
 
         int flingAxis = 0;
+        int horizontalAxis = 0;
+        int verticalAxis = 0;
         boolean useSmoothScroll = false;
         if (event.getAction() == MotionEvent.ACTION_SCROLL) {
             final float vScroll, hScroll;
@@ -4055,11 +4128,13 @@
                     // Inverse the sign of the vertical scroll to align the scroll orientation
                     // with AbsListView.
                     vScroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                    verticalAxis = MotionEvent.AXIS_VSCROLL;
                 } else {
                     vScroll = 0f;
                 }
                 if (mLayout.canScrollHorizontally()) {
                     hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+                    horizontalAxis = MotionEvent.AXIS_HSCROLL;
                 } else {
                     hScroll = 0f;
                 }
@@ -4069,10 +4144,12 @@
                     // Invert the sign of the vertical scroll to align the scroll orientation
                     // with AbsListView.
                     vScroll = -axisScroll;
+                    verticalAxis = MotionEvent.AXIS_SCROLL;
                     hScroll = 0f;
                 } else if (mLayout.canScrollHorizontally()) {
                     vScroll = 0f;
                     hScroll = axisScroll;
+                    horizontalAxis = MotionEvent.AXIS_SCROLL;
                 } else {
                     vScroll = 0f;
                     hScroll = 0f;
@@ -4097,7 +4174,8 @@
                 smoothScrollBy(scaledHScroll, scaledVScroll, /* interpolator= */ null,
                         UNDEFINED_DURATION, /* withNestedScrolling= */ true);
             } else {
-                nestedScrollByInternal(scaledHScroll, scaledVScroll, event, TYPE_NON_TOUCH);
+                nestedScrollByInternal(scaledHScroll, scaledVScroll, horizontalAxis,
+                        verticalAxis, event, TYPE_NON_TOUCH);
             }
 
             if (flingAxis != 0 && !useSmoothScroll) {