Repository: apache/solr
Pull request #3282: SOLR-17705 for V2 API
Author: login: dsmiley
Labels: name: test-framework
name: client:solrj
name: tests
name: cat:cloud
name: cat:cli
name: cat:packagemanager
Created: 2025-03-22T19:19:58Z | Merged: 2025-04-17T14:10:47Z

https://issues.apache.org/jira/browse/SOLR-17705

Moves JSON parsing into the ResponseParser where it belongs.
Removes generated intermediate SolrResponse layer classes.

This PR is branched off of another PR, #3270, and so it includes all the changes there.  Once that merges, this one will show what's new.  I recommend looking at only the recent commit.  And since it updates generated code, a real review requires checking out the code to generate and look at it.


Diff:
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 2e93c65d47a7..1dc3ecc895ab 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -41,7 +41,9 @@ Improvements
 
 * SOLR-17701: Simplify bin/solr scripts by removing AUTH_PORT export in favour of more commonly used SOLR_PORT export. (Eric Pugh)
 
-* SOLR-17705: SolrJ's SolrRequest now allows any type for its response; needn't be SolrResponse. (David Smiley)
+* SOLR-17705: SolrJ's SolrRequest now allows any type for its response; needn't be SolrResponse.
+  Simplified the V2 API to remove the needless SolrResponse.  And moved JSON/Jackson binding into
+  a new JacksonDataBindResponseParser (David Smiley)
 
 Optimizations
 ---------------------
@@ -203,7 +205,7 @@ New Features
 
 Improvements
 ---------------------
-* SOLR-15751: The v2 API now has parity with the v1 "COLSTATUS" and "segments" APIs, which can be used to fetch detailed information about 
+* SOLR-15751: The v2 API now has parity with the v1 "COLSTATUS" and "segments" APIs, which can be used to fetch detailed information about
   specific collections or cores.  Collection information can be fetched by a call to `GET /api/collections/collectionName`, and core
   information with a call to `GET /api/cores/coreName/segments`. (Jason Gerlowski)
 
@@ -262,7 +264,7 @@ Dependency Upgrades
 
 Other Changes
 ---------------------
-* SOLR-17579: Remove unused code and other refactorings in ReplicationHandler and tests.  Removed unused public 
+* SOLR-17579: Remove unused code and other refactorings in ReplicationHandler and tests.  Removed unused public
   LOCAL_ACTIVITY_DURING_REPLICATION variable. (Eric Pugh)
 
 * GITHUB#2869: SolrTestCase now supports @LogLevel annotations (as SolrTestCaseJ4 has).  Added LogLevelTestRule
diff --git a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
index 0f9f8641ac58..7bde598f1d22 100644
--- a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
+++ b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
@@ -320,7 +320,7 @@ public static boolean safeCheckCoreExists(String solrUrl, String coreName, Strin
           Thread.sleep(clamPeriodForStatusPollMs);
         }
         final var coreStatusReq = new CoresApi.GetCoreStatus(coreName);
-        final var coreStatusRsp = coreStatusReq.process(solrClient).getParsed();
+        final var coreStatusRsp = coreStatusReq.process(solrClient);
         final var coreStatusByName = coreStatusRsp.status;
         final var coreStatus = coreStatusByName.get(coreName);
         final var failureStatus = coreStatusRsp.initFailures;
diff --git a/solr/core/src/java/org/apache/solr/filestore/DistribFileStore.java b/solr/core/src/java/org/apache/solr/filestore/DistribFileStore.java
index 96b81992d4c9..0e654491832b 100644
--- a/solr/core/src/java/org/apache/solr/filestore/DistribFileStore.java
+++ b/solr/core/src/java/org/apache/solr/filestore/DistribFileStore.java
@@ -239,7 +239,7 @@ boolean fetchFromAnyNode() {
               coreContainer.getZkController().getZkStateReader().getBaseUrlV2ForNodeName(liveNode);
           final var metadataRequest = new FileStoreApi.GetMetadata(path);
           final var client = coreContainer.getSolrClientCache().getHttpSolrClient(baseUrl);
-          final var metadataResponse = metadataRequest.process(client).getParsed();
+          final var metadataResponse = metadataRequest.process(client);
           boolean nodeHasBlob =
               metadataResponse.files != null && metadataResponse.files.containsKey(path);
 
@@ -387,7 +387,7 @@ private void distribute(FileInfo info) {
           pullFileRequest.setGetFrom(nodeToFetchFrom);
           final var client = coreContainer.getSolrClientCache().getHttpSolrClient(baseUrl);
           // fire and forget
-          pullFileRequest.process(client).getParsed();
+          pullFileRequest.process(client);
         } catch (Exception e) {
           log.info("Node: {} failed to respond for file fetch notification", node, e);
           // ignore the exception
diff --git a/solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java b/solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java
index b0bb80cef421..4e3aaa5ab710 100644
--- a/solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java
+++ b/solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java
@@ -35,6 +35,7 @@
 import org.apache.commons.io.IOUtils;
 import org.apache.lucene.util.SuppressForbidden;
 import org.apache.solr.cli.CLIUtils;
+import org.apache.solr.client.api.model.UploadToFileStoreResponse;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -96,7 +97,7 @@ public static void postFile(SolrClient client, ByteBuffer buffer, String name, S
         uploadReq.setSig(List.of(sig));
       }
 
-      final var uploadRsp = uploadReq.process(client).getParsed();
+      final UploadToFileStoreResponse uploadRsp = uploadReq.process(client);
       if (!name.equals(uploadRsp.file)) {
         throw new SolrException(
             ErrorCode.BAD_REQUEST,
diff --git a/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java b/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java
index fb8c03c7d0b6..b56d6441709c 100644
--- a/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java
+++ b/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java
@@ -151,7 +151,7 @@ public void addKey(byte[] key, String destinationKeyFilename) throws Exception {
     String path = ClusterFileStore.KEYS_DIR + "/" + destinationKeyFilename;
     PackageUtils.uploadKey(key, path, Path.of(solrHome));
     final var syncRequest = new FileStoreApi.SyncFile(path);
-    final var syncResponse = syncRequest.process(solrClient).getParsed();
+    final var syncResponse = syncRequest.process(solrClient);
     final var status = syncResponse.responseHeader.status;
     if (status != 0) {
       throw new SolrException(
diff --git a/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java b/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java
index 0742bf851155..6ce8503323dc 100644
--- a/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/CollectionsAPISolrJTest.java
@@ -797,9 +797,7 @@ public void testV2BasicCollectionStatus() throws Exception {
     indexSomeDocs(simpleCollName);
 
     final var simpleResponse =
-        new CollectionsApi.GetCollectionStatus(simpleCollName)
-            .process(cluster.getSolrClient())
-            .getParsed();
+        new CollectionsApi.GetCollectionStatus(simpleCollName).process(cluster.getSolrClient());
     assertEquals(simpleCollName, simpleResponse.name);
     assertEquals(2, simpleResponse.shards.size());
     assertEquals(Integer.valueOf(2), simpleResponse.activeShards);
@@ -814,7 +812,7 @@ public void testV2BasicCollectionStatus() throws Exception {
     // Ensure segment data present when request sets 'segments=true' flag
     final var segmentDataRequest = new CollectionsApi.GetCollectionStatus(simpleCollName);
     segmentDataRequest.setSegments(true);
-    final var segmentDataResponse = segmentDataRequest.process(cluster.getSolrClient()).getParsed();
+    final var segmentDataResponse = segmentDataRequest.process(cluster.getSolrClient());
     var segmentData = segmentDataResponse.shards.get("shard1").leader.segInfos;
     assertNotNull(segmentData);
     assertTrue(segmentData.info.numSegments > 0); // Expect at least one segment
@@ -827,8 +825,7 @@ public void testV2BasicCollectionStatus() throws Exception {
     // Ensure file-size data present when request sets sizeInfo flag
     final var segmentFileSizeRequest = new CollectionsApi.GetCollectionStatus(simpleCollName);
     segmentFileSizeRequest.setSizeInfo(true);
-    final var segmentFileSizeResponse =
-        segmentFileSizeRequest.process(cluster.getSolrClient()).getParsed();
+    final var segmentFileSizeResponse = segmentFileSizeRequest.process(cluster.getSolrClient());
     segmentData = segmentFileSizeResponse.shards.get("shard1").leader.segInfos;
     assertNotNull(segmentData);
     final var largeFileList = segmentData.segments.get("_0").largestFilesByName;
diff --git a/solr/core/src/test/org/apache/solr/filestore/TestDistribFileStore.java b/solr/core/src/test/org/apache/solr/filestore/TestDistribFileStore.java
index 49a77b9a02e0..bff833f8c549 100644
--- a/solr/core/src/test/org/apache/solr/filestore/TestDistribFileStore.java
+++ b/solr/core/src/test/org/apache/solr/filestore/TestDistribFileStore.java
@@ -324,7 +324,7 @@ public static void uploadKey(byte[] bytes, String path, MiniSolrCloudCluster clu
       final var syncReq = new FileStoreApi.SyncFile(path);
       final var syncRsp = syncReq.process(client);
       if (log.isInfoEnabled()) {
-        log.info("sync resp for path {} was {}", path, syncRsp.getParsed().responseHeader.status);
+        log.info("sync resp for path {} was {}", path, syncRsp.responseHeader.status);
       }
     }
     checkAllNodesForFile(
diff --git a/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java b/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java
index 1a1d0ac3265a..ad8bdcc792fb 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java
@@ -1669,7 +1669,7 @@ private Date watchCoreStartAt(JettySolrRunner jettySolrRunner, final Date min)
       final var statusRequest = new CoresApi.GetCoreStatus("collection1");
       while (timeSlept < TIMEOUT) {
         try {
-          final var statusResponse = statusRequest.process(adminClient).getParsed();
+          final var statusResponse = statusRequest.process(adminClient);
           assertNotNull(statusResponse.status);
           assertTrue(statusResponse.status.containsKey("collection1"));
           final var coreStatus = statusResponse.status.get("collection1");
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperReadAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperReadAPITest.java
index 41eddba832e3..31609dc8318a 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperReadAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperReadAPITest.java
@@ -76,8 +76,7 @@ public void testZkread() throws Exception {
       }
 
       final var configListRequest = new ZookeeperReadApi.ListNodes("/configs");
-      final var configListResponse = configListRequest.process(client).getParsed();
-
+      final var configListResponse = configListRequest.process(client);
       assertEquals(
           6, configListResponse.unknownProperties().get("/configs").get("_default").children);
       assertEquals(2, configListResponse.unknownProperties().get("/configs").get("conf").children);
@@ -133,7 +132,7 @@ public void testRequestingDataFromNonexistentNodeReturnsAnError() throws Excepti
   public void testCanListChildNodes() throws Exception {
     try (HttpSolrClient client = new HttpSolrClient.Builder(baseUrl.toString()).build()) {
       final var listDefaultFilesReq = new ZookeeperReadApi.ListNodes("/configs/_default");
-      final var listDefaultFilesResponse = listDefaultFilesReq.process(client).getParsed();
+      final var listDefaultFilesResponse = listDefaultFilesReq.process(client);
 
       // At the top level, the response contains a key with the value of the specified zkPath
       assertEquals(1, listDefaultFilesResponse.unknownProperties().size());
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java b/solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
new file mode 100644
index 000000000000..5ea1ed2e287c
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/**
+ * Parses JSON, deserializing to a domain type (class) via Jackson data-bind.
+ *
+ * @lucene.experimental
+ */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
+  }
+
+  @Override
+  public Collection<String> getContentTypes() {
+    return List.of("application/json");
+  }
+
+  // TODO it'd be nice if the ResponseParser could receive the mime type so it can parse
+  //  accordingly, maybe json, cbor, smile
+
+  @Override
+  public NamedList<Object> processResponse(InputStream stream, String encoding) throws IOException {
+    // TODO SOLR-17549 for error handling, implying a significant ResponseParser API change
+    // TODO generalize to CBOR, Smile, ...
+    var mapper = JacksonContentWriter.DEFAULT_MAPPER;
+
+    final T parsedVal;
+    if (encoding == null) {
+      parsedVal = mapper.readValue(stream, typeParam);
+    } else {
+      parsedVal = mapper.readValue(new InputStreamReader(stream, encoding), typeParam);
+    }
+
+    return SimpleOrderedMap.of("response", parsedVal);
+  }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/JacksonParsingResponse.java b/solr/solrj/src/java/org/apache/solr/client/solrj/JacksonParsingResponse.java
deleted file mode 100644
index 3e95da59657c..000000000000
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/JacksonParsingResponse.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
-
-import java.io.InputStream;
-import org.apache.solr.client.solrj.response.SimpleSolrResponse;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.ObjectReleaseTracker;
-
-public class JacksonParsingResponse<T> extends SimpleSolrResponse {
-
-  private final Class<T> typeParam;
-
-  public JacksonParsingResponse(Class<T> typeParam) {
-    this.typeParam = typeParam;
-  }
-
-  public T getParsed() {
-    final NamedList<Object> resp = getResponse();
-    final var stream = (InputStream) resp.get("stream");
-    try {
-      final T parsedVal = JacksonContentWriter.DEFAULT_MAPPER.readValue(stream, typeParam);
-      assert ObjectReleaseTracker.release(stream);
-      return parsedVal;
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-}
diff --git a/solr/solrj/src/resources/java-template/api.mustache b/solr/solrj/src/resources/java-template/api.mustache
index 64cfef3999b1..2239f9f95070 100644
--- a/solr/solrj/src/resources/java-template/api.mustache
+++ b/solr/solrj/src/resources/java-template/api.mustache
@@ -28,13 +28,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-
+import org.apache.solr.client.solrj.JacksonDataBindResponseParser;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.response.SimpleSolrResponse;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.client.solrj.JacksonParsingResponse;
 import org.apache.solr.client.solrj.JacksonContentWriter;
 import org.apache.solr.client.solrj.request.RequestWriter.ContentWriter;
 import org.apache.solr.client.solrj.impl.InputStreamResponseParser;
@@ -77,7 +76,7 @@ import {{modelPackage}}.{{dataType}};
 // changes should be made to the underlying template and not this file directly.
 
 /**
- * Experimental SolrRequest's and SolrResponse's for {{classVarName}}, generated from an OAS.
+ * Experimental {@link SolrRequest}s for {{classVarName}}, generated from an OAS.
  *
  * <p>See individual request and response classes for more detailed and relevant information.
  *
@@ -91,17 +90,13 @@ public class {{classname}} {
     {{#operation}}
     {{^vendorExtensions.x-omitFromCodegen}}
         {{#vendorExtensions.x-rawOutput}}
-        public static class {{operationIdCamelCase}}Response extends InputStreamResponse {}
+            public static class {{operationIdCamelCase}} extends SolrRequest
+          <InputStreamResponse> {
         {{/vendorExtensions.x-rawOutput}}
         {{^vendorExtensions.x-rawOutput}}
-        public static class {{operationIdCamelCase}}Response extends JacksonParsingResponse<{{modelPackage}}.{{returnType}}> {
-            public {{operationIdCamelCase}}Response() {
-                super({{modelPackage}}.{{returnType}}.class);
-            }
-        }
+            public static class {{operationIdCamelCase}} extends SolrRequest<{{modelPackage}}
+            .{{returnType}}> {
         {{/vendorExtensions.x-rawOutput}}
-
-        public static class {{operationIdCamelCase}} extends SolrRequest<{{operationIdCamelCase}}Response> {
             {{#bodyParam}}
             private final {{{dataType}}} requestBody;
             {{/bodyParam}}
@@ -259,17 +254,31 @@ public class {{classname}} {
               {{/queryParams}}
               return queryParams;
             }
+        {{#vendorExtensions.x-rawOutput}}
+            @Override
+          protected InputStreamResponse createResponse(NamedList
+          <Object> namedList) {
+            return new InputStreamResponse();
+            }
 
             @Override
-          protected {{operationIdCamelCase}}Response createResponse(NamedList
-        <Object> namedList) {
-              return new {{operationIdCamelCase}}Response();
+            public InputStreamResponseParser getResponseParser() {
+            return new InputStreamResponseParser("json");
+            }
+        {{/vendorExtensions.x-rawOutput}}
+        {{^vendorExtensions.x-rawOutput}}
+            @Override
+            protected {{modelPackage}}.{{returnType}} createResponse(NamedList
+          <Object> namedList) {
+            return ({{modelPackage}}.{{returnType}}) namedList.get("response");
             }
 
             @Override
             public ResponseParser getResponseParser() {
-              return new InputStreamResponseParser("json");
+            return new JacksonDataBindResponseParser<>({{modelPackage}}.{{returnType}}.class);
             }
+        {{/vendorExtensions.x-rawOutput}}
+
         }
     {{/vendorExtensions.x-omitFromCodegen}}
     {{/operation}}
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/ApiMustacheTemplateTests.java b/solr/solrj/src/test/org/apache/solr/client/solrj/ApiMustacheTemplateTests.java
deleted file mode 100644
index 1dd0c5240538..000000000000
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/ApiMustacheTemplateTests.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.solrj.request.CollectionsApi.ListCollectionsResponse;
-import org.apache.solr.common.util.NamedList;
-import org.junit.Assert;
-import org.junit.Test;
-
-/**
- * A test ensuring that specific generated SolrRequest classes deserialize responses into the
- * correct type.
- *
- * <p>See SOLR-17326 for more context. Consider removing once SOLR-17329 has been completed.
- */
-public class ApiMustacheTemplateTests {
-
-  /**
-   * Tests the return type in generated response classes. This test ensures that responses are still
-   * returning API models in case of a naming conflict between response class and return type (API
-   * model).
-   */
-  @Test
-  public void testParsedReturnTypes() {
-    JacksonParsingResponse<?> response = getJacksonParsingResponse();
-
-    Object data = null;
-    try {
-      // Parsing may fail if wrong class is used
-      data = response.getParsed();
-    } catch (Exception e) {
-      // Parsing should not fail for the given example.
-      Assert.fail("Response parsing failed with error " + e.getMessage());
-    }
-
-    // In case of a naming conflict, even if parsing would succeed, the type would still be the same
-    // as response
-    Assert.assertNotSame(data.getClass(), response.getClass());
-    Assert.assertFalse(data instanceof JacksonParsingResponse<?>);
-
-    // Currently, all response types extend SolrJerseyResponse. Adjust if this change in the future.
-    Assert.assertTrue(data instanceof SolrJerseyResponse);
-  }
-
-  /**
-   * @return Returns a dummy response of {@link ListCollectionsResponse} with data.
-   */
-  private static JacksonParsingResponse<?> getJacksonParsingResponse() {
-    JacksonParsingResponse<?> response = new ListCollectionsResponse(); // API response
-
-    // Provide a dummy response for ListCollectionsResponse
-    InputStream inputStream =
-        new ByteArrayInputStream(
-            "{\"responseHeader\":{\"status\":0,\"QTime\":0},\"collections\":[\"testCollection\"]}"
-                .getBytes(StandardCharsets.UTF_8));
-    NamedList<Object> responseData = new NamedList<>();
-    responseData.add("stream", inputStream);
-    response.setResponse(responseData);
-
-    return response;
-  }
-}
diff --git a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
index 49e2f3139fcd..12baf8a0e376 100644
--- a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
+++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
@@ -712,7 +712,7 @@ public void dumpCoresInfo(PrintStream pw) throws IOException {
       final var coreStatusReq = new CoresApi.GetAllCoreStatus();
       coreStatusReq.setIndexInfo(true);
       try (final var client = newClient()) {
-        final var coreStatusRsp = coreStatusReq.process(client).getParsed();
+        final var coreStatusRsp = coreStatusReq.process(client);
         Utils.writeJson(coreStatusRsp, pw, true);
       } catch (SolrServerException | IOException e) {
         // Worth logging but not re-throwing


Reviews:
user: login: dsmiley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T19:25:34Z
user: login: gerlowskija
body: I left a few comments inline.  No huge issues, but a few ideas and questions.  Curious what you think!

What's the relationship between this PR and the one that underlies it?  Are you looking for consensus on both before you merge the underlying one?  (Mostly asking to make sure I'm directing my review effort in the right place to keep you unblocked...)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T15:29:09Z
user: login: dsmiley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T18:05:54Z
user: login: dsmiley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T18:07:11Z
user: login: dsmiley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T18:30:15Z
user: login: dsmiley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T18:38:41Z
user: login: gerlowskija
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T11:26:26Z
user: login: gerlowskija
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T11:28:46Z
user: login: gerlowskija
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T11:32:23Z
user: login: dsmiley
body: This PR should be ready to merge.  Okay @gerlowskija ?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T17:59:13Z

Review comments:
diff_hunk: @@ -322,7 +322,7 @@ public static boolean safeCheckCoreExists(String solrUrl, String coreName, Strin
           Thread.sleep(clamPeriodForStatusPollMs);
         }
         final var coreStatusReq = new CoresApi.GetCoreStatus(coreName);
-        final var coreStatusRsp = coreStatusReq.process(solrClient).getParsed();
path: solr/core/src/java/org/apache/solr/cli/CLIUtils.java
user: login: dsmiley
body: I admit the result of this work as seen in consuming/calling code right here is _merely_ removing `getParsed()` since there is now no intermediate class holding the parsed value.  But anyone browsing the generated code / class hierarchy will now see something simpler.
created_at: 2025-03-22T19:21:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,7 +97,7 @@ public static void postFile(SolrClient client, ByteBuffer buffer, String name, S
         uploadReq.setSig(List.of(sig));
       }
 
-      final var uploadRsp = uploadReq.process(client).getParsed();
+      final UploadToFileStoreResponse uploadRsp = uploadReq.process(client);
path: solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java
user: login: dsmiley
body: IMO we've been using `var` too much.
created_at: 2025-03-22T19:22:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,17 +90,13 @@ public class {{classname}} {
     {{#operation}}
     {{^vendorExtensions.x-omitFromCodegen}}
         {{#vendorExtensions.x-rawOutput}}
-        public static class {{operationIdCamelCase}}Response extends InputStreamResponse {}
path: solr/solrj/src/resources/java-template/api.mustache
user: login: dsmiley
body: gone!
created_at: 2025-03-22T19:24:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,17 +90,13 @@ public class {{classname}} {
     {{#operation}}
     {{^vendorExtensions.x-omitFromCodegen}}
         {{#vendorExtensions.x-rawOutput}}
-        public static class {{operationIdCamelCase}}Response extends InputStreamResponse {}
+            public static class {{operationIdCamelCase}} extends SolrRequest
+          <InputStreamResponse> {
         {{/vendorExtensions.x-rawOutput}}
         {{^vendorExtensions.x-rawOutput}}
-        public static class {{operationIdCamelCase}}Response extends JacksonParsingResponse<{{modelPackage}}.{{returnType}}> {
path: solr/solrj/src/resources/java-template/api.mustache
user: login: dsmiley
body: gone!
created_at: 2025-03-22T19:24:08Z
author_association: CONTRIBUTOR
path: solr/solrj/src/test/org/apache/solr/client/solrj/ApiMustacheTemplateTests.java
user: login: dsmiley
body: seems obsolete now
created_at: 2025-03-22T19:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,7 +97,7 @@ public static void postFile(SolrClient client, ByteBuffer buffer, String name, S
         uploadReq.setSig(List.of(sig));
       }
 
-      final var uploadRsp = uploadReq.process(client).getParsed();
+      final UploadToFileStoreResponse uploadRsp = uploadReq.process(client);
path: solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java
user: login: gerlowskija
body: Haha, you turned me on to it a year or two back and I've loved it since.  I get that the type is less visible, but it makes the code scan better in places where the type name is a bit of a mouthful and "tidy" would otherwise split it up into multiple lines

But, to each their own.
created_at: 2025-04-02T18:21:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: gerlowskija
body: [0] Maybe remove "DataBind" from the name, as it's implied/redundant once Jackson is mentioned?

[-0] Wdyt about marking this class "lucene.experimental" so we can rename or make changes more freely later if needed.  I guess we're still held to the ResponseParser interface, so things couldn't change too drastically, but it still might be prudent since this is something that we're still trying to get right in SolrJ.
created_at: 2025-04-03T12:05:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: gerlowskija
body: [0] I think this would need to be changed at the ResponseParser level, but it's a bummer that this method can't return a list, or indicate handling for multiple write-types.  There's no reason our Jackson support should be specific to JSON, as you indicate in some comments below.

Much larger issue than this PR though...
created_at: 2025-04-03T12:08:28Z
author_association: CONTRIBUTOR
path: solr/solrj/src/test/org/apache/solr/client/solrj/ApiMustacheTemplateTests.java
user: login: gerlowskija
body: [0] It's obsolete in its current form, but in theory it still makes sense to have *some* sort of test validating that the v2 deserialization plumbing actually works as intended?

We use the generated v2 code in a few places (e.g. CLIUtils, TestDistribFileStore), so I guess there's some implicit testing of this already?  Maybe that's enough? idk - just thinking aloud. 
created_at: 2025-04-03T13:02:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,13 +24,13 @@
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
 
 /**
  * A class for making a request to the config handler. Tests can use this e.g. to add custom
  * components, handlers, parsers, etc. to an otherwise generic configset.
  */
-@SuppressWarnings({"rawtypes"})
-public class ConfigRequest extends CollectionRequiringSolrRequest {
+public class ConfigRequest extends CollectionRequiringSolrRequest<SolrResponse> {
path: solr/test-framework/src/java/org/apache/solr/cloud/ConfigRequest.java
user: login: gerlowskija
body: [0] Unrelated to your PR, but I wonder why this lives in `test-framework`...
created_at: 2025-04-03T13:03:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
+  }
+
+  @Override
+  public Collection<String> getContentTypes() {
+    return List.of("application/json");
+  }
+
+  // TODO it'd be nice if the ResponseParser could receive the mime type so it can parse
+  //  accordingly, maybe json, cbor, smile
+
+  @Override
+  public NamedList<Object> processResponse(InputStream stream, String encoding) throws IOException {
+    // TODO generalize to CBOR, Smile, ...
+    var mapper = JacksonContentWriter.DEFAULT_MAPPER;
+
+    final T parsedVal;
+    if (encoding == null) {
+      parsedVal = mapper.readValue(stream, typeParam);
+    } else {
+      parsedVal = mapper.readValue(new InputStreamReader(stream, encoding), typeParam);
+    }
+
+    return SimpleOrderedMap.of("response", parsedVal);
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: gerlowskija
body: [0] `processResponse`'s callers (`HttpSolrClientBase`, `HttpSolrClient`) do some error-checking on this method's retval, looking for an "error" key in the NamedList as an indicator of error.  This implementation will never return a NL with that key, so it loses the benefits of that error-checking.

IMO we needn't handle that in this PR.  It pre-exists your change in InputStreamResponseParser, NoOpResponseParser, etc.  And there's [a JIRA ticket](https://issues.apache.org/jira/browse/SOLR-17549) for thinking through how we want error-handling to work in v2, so it won't get lost or fall through the cracks.

But I'm curious if you had any thoughts.

I had a plan in the back of my mind of uniting JacksonParsingResponse and InputStreamResponse, which would allow us to hook into some of the validation the ISR currently has.  But it was never a great plan, and it doesn't make any sense if JPR goes away entirely.  It'd be hacky, but we could have callers look for `instanceof SolrJerseyResponse` and inspect that POJO for errors?  Or maybe the client only looks at the HTTP status code for v2 APIs? 


created_at: 2025-04-03T13:38:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
+  }
+
+  @Override
+  public Collection<String> getContentTypes() {
+    return List.of("application/json");
+  }
+
+  // TODO it'd be nice if the ResponseParser could receive the mime type so it can parse
+  //  accordingly, maybe json, cbor, smile
+
+  @Override
+  public NamedList<Object> processResponse(InputStream stream, String encoding) throws IOException {
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: gerlowskija
body: [0] I love that `process` can now return arbitrary types; it's a shame that can't extend to the retval of `processResponse`, and that we have to repackage our arbitrary type as a NL for all of the various SolrClient-internal NL-inspection code.

I think I brought this up in the related PR, but it strikes me again whether we shouldn't be slightly stricter on the allowed return types.  Allow `process` to return types other than NL, but require retvals to implement some interface that exposes the information that most of our SolrClient impls look at internally:  `wasSuccessful()`, `getErrorMessage()`, etc.  That'd give our clients a uniform way to inspect responses and throw SolrServerExceptions, etc, regardless of whether we parse the response into a NamedList, or a SolrJerseyResponse, or an InputStreamResponse, or...

(This would break the error-handling logjam I've been wrestling with for awhile now too - see comment on L62 for more details.)
created_at: 2025-04-03T13:48:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: dsmiley
body: Jackson does not imply Jackson-DataBind -- the latter depends on the former, of course.  Jackson is the parsing, DataBind is the, well, data binding -- mapping to POJOs via annotations etc.

Happy to add lucene.experimental wherever.
created_at: 2025-04-03T18:05:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: dsmiley
body: That's a separate issue for sure, and was on my mind a bit as I worked on this
created_at: 2025-04-03T18:07:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
+  }
+
+  @Override
+  public Collection<String> getContentTypes() {
+    return List.of("application/json");
+  }
+
+  // TODO it'd be nice if the ResponseParser could receive the mime type so it can parse
+  //  accordingly, maybe json, cbor, smile
+
+  @Override
+  public NamedList<Object> processResponse(InputStream stream, String encoding) throws IOException {
+    // TODO generalize to CBOR, Smile, ...
+    var mapper = JacksonContentWriter.DEFAULT_MAPPER;
+
+    final T parsedVal;
+    if (encoding == null) {
+      parsedVal = mapper.readValue(stream, typeParam);
+    } else {
+      parsedVal = mapper.readValue(new InputStreamReader(stream, encoding), typeParam);
+    }
+
+    return SimpleOrderedMap.of("response", parsedVal);
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: dsmiley
body: Ah; good catch!  Hmm.  It'd be good to induce an error in a test using this API so we can verify that whatever we want to happen, actually happens.  I would personally rather prefer that become a new evolutionary issue/PR rather than increasing the scope of this one.  I could see us making bigger changes to accommodate that concern.  I'll expand on that in the other comment...
created_at: 2025-04-03T18:30:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
+  }
+
+  @Override
+  public Collection<String> getContentTypes() {
+    return List.of("application/json");
+  }
+
+  // TODO it'd be nice if the ResponseParser could receive the mime type so it can parse
+  //  accordingly, maybe json, cbor, smile
+
+  @Override
+  public NamedList<Object> processResponse(InputStream stream, String encoding) throws IOException {
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: dsmiley
body: I would honestly hate to bring back a root type merely for error handling.  Java has exceptions for this; let's use that instead.  We could make processResponse return whatever and for successful responses and throw something for the error case.  That's a bigger change than the scope of this PR, so WDYT about merging this without additional error handling?

I propose a new JIRA issue (or your existing SOLR-17549 maybe) enhance the `ResponseParser` abstraction to take on the responsibility of error detection and throwing, and thus reducing some of the scope in the HTTP SolrClient side.  I could imagine the processResponse method having more parameters including the HTTP status code, and using that to determine are we going to use data-bind or are we going to look for the error.  We'd need to do this for XML & JavaBin; finding a way to reuse some logic where it's similar.  Don't need to spec this out this second.
created_at: 2025-04-03T18:38:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: gerlowskija
body: > Jackson is the parsing, DataBind is the, well, data binding -- mapping to POJOs via annotations etc.

Oh, OK, fair enough.

I've only ever used Jackson with those annotations - I know in theory that the dependencies come separately, but it's hard for me to imagine how you'd even use Jackson if you're not converting to+from some POJO.  Is it just all `Map` and `List` instances without databind?  (That's rhetorical I guess - I can google it.)
created_at: 2025-04-09T11:26:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
+  }
+
+  @Override
+  public Collection<String> getContentTypes() {
+    return List.of("application/json");
+  }
+
+  // TODO it'd be nice if the ResponseParser could receive the mime type so it can parse
+  //  accordingly, maybe json, cbor, smile
+
+  @Override
+  public NamedList<Object> processResponse(InputStream stream, String encoding) throws IOException {
+    // TODO generalize to CBOR, Smile, ...
+    var mapper = JacksonContentWriter.DEFAULT_MAPPER;
+
+    final T parsedVal;
+    if (encoding == null) {
+      parsedVal = mapper.readValue(stream, typeParam);
+    } else {
+      parsedVal = mapper.readValue(new InputStreamReader(stream, encoding), typeParam);
+    }
+
+    return SimpleOrderedMap.of("response", parsedVal);
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: gerlowskija
body: > I would personally rather prefer that become a new evolutionary issue/PR rather than increasing the scope of this one

Sounds good - we've already got this tracked in SOLR-17549, so we're good on that front.
created_at: 2025-04-09T11:28:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.client.solrj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+/** Parses JSON, deserializing to a domain type (class) via Jackson data-bind. */
+public class JacksonDataBindResponseParser<T> extends ResponseParser {
+
+  private final Class<T> typeParam;
+
+  public JacksonDataBindResponseParser(Class<T> typeParam) {
+    this.typeParam = typeParam;
+  }
+
+  @Override
+  public String getWriterType() {
+    return "json";
+  }
+
+  @Override
+  public Collection<String> getContentTypes() {
+    return List.of("application/json");
+  }
+
+  // TODO it'd be nice if the ResponseParser could receive the mime type so it can parse
+  //  accordingly, maybe json, cbor, smile
+
+  @Override
+  public NamedList<Object> processResponse(InputStream stream, String encoding) throws IOException {
path: solr/solrj/src/java/org/apache/solr/client/solrj/JacksonDataBindResponseParser.java
user: login: gerlowskija
body: I like that overall plan - it makes sense conceptually to have `ResponseParser` do error detection, since that's part of the response.  (And it'd be a really nice simplification for our SolrClient implementations themselves.)

And as mentioned elsewhere - it's fine with me to not put that in this PR.  It's definitely a bigger change.
created_at: 2025-04-09T11:32:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: dsmiley
created_at: 2025-03-28T05:28:39Z
body: WDYT @gerlowskija ?
author_association: CONTRIBUTOR
user: login: dsmiley
created_at: 2025-04-03T18:39:44Z
body: > What's the relationship between this PR and the one that underlies it? Are you looking for consensus on both before you merge the underlying one? 

Yes; these are important changes that require peer review.  I don't want to be a cowboy here, doing whatever to important APIs.
author_association: CONTRIBUTOR
user: login: dsmiley
created_at: 2025-04-09T14:27:00Z
body: Good.  After merging the parent issue tonight, I'll brush up this PR, adding lucene.experimental but mostly unchanged, update CHANGES.txt.  Should be in mergeable shape tomorrow.
author_association: CONTRIBUTOR
user: login: dsmiley
created_at: 2025-04-15T03:07:01Z
body: Well I'll merge this in another day if I don't hear back.
author_association: CONTRIBUTOR
user: login: gerlowskija
created_at: 2025-04-17T11:10:59Z
body: Sorry, was out on some PTO.

This all LGTM; merge away!
author_association: CONTRIBUTOR