This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new f1645e6e55 TIKA-4840: host-provided Jackson for pipes plugins; one 
strict Plugin… (#3066)
f1645e6e55 is described below

commit f1645e6e55332839fcb866a117b07e82ed506ac9
Author: Tim Allison <[email protected]>
AuthorDate: Tue Aug 25 20:38:29 2026 -0400

    TIKA-4840: host-provided Jackson for pipes plugins; one strict Plugin… 
(#3066)
---
 CHANGES.txt                                        | 10 +++
 .../ROOT/pages/pipes/plugins/writing-a-plugin.adoc | 24 ++++++-
 tika-pipes/tika-pipes-plugins/pom.xml              | 23 ++++++-
 .../tika-pipes-atlassian-jwt/pom.xml               |  8 ---
 .../config/AtlassianJwtFetcherConfig.java          | 13 +---
 .../tika-pipes-plugins/tika-pipes-az-blob/pom.xml  |  4 --
 .../pipes/emitter/azblob/AZBlobEmitterConfig.java  | 12 +---
 .../fetcher/azblob/config/AZBlobFetcherConfig.java | 13 +---
 .../iterator/azblob/AZBlobPipesIteratorConfig.java | 14 +---
 .../pipes/iterator/csv/CSVPipesIteratorConfig.java | 14 +---
 .../tika/pipes/emitter/es/ESEmitterConfig.java     | 13 +---
 .../tika/pipes/reporter/es/ESReporterConfig.java   | 14 +---
 .../tika-pipes-file-system/pom.xml                 |  5 --
 .../pipes/emitter/fs/FileSystemEmitterConfig.java  | 14 +---
 .../emitter/fs/FileSystemEmitterRuntimeConfig.java | 14 +---
 .../pipes/fetcher/fs/FileSystemFetcherConfig.java  | 14 +---
 .../iterator/fs/FileSystemPipesIteratorConfig.java | 15 +---
 .../reporter/fs/FileSystemReporterConfig.java      | 14 +---
 .../pipes/fetcher/fs/FileSystemFetcherTest.java    | 13 ++++
 .../tika/pipes/emitter/gcs/GCSEmitterConfig.java   | 12 +---
 .../pipes/fetcher/gcs/config/GCSFetcherConfig.java | 13 +---
 .../pipes/iterator/gcs/GCSPipesIteratorConfig.java | 13 +---
 .../tika-pipes-google-drive/pom.xml                |  8 ---
 .../config/GoogleDriveFetcherConfig.java           | 13 +---
 .../tika-pipes-plugins/tika-pipes-http/pom.xml     |  8 ---
 .../fetcher/http/config/HttpFetcherConfig.java     | 13 +---
 .../tika/pipes/emitter/jdbc/JDBCEmitterConfig.java | 13 +---
 .../iterator/jdbc/JDBCPipesIteratorConfig.java     | 14 +---
 .../reporter/jdbc/JDBCPipesReporterConfig.java     | 13 +---
 .../json/JsonPipesIteratorConfig.java              | 14 +---
 .../tika-pipes-plugins/tika-pipes-kafka/pom.xml    |  5 --
 .../pipes/emitter/kafka/KafkaEmitterConfig.java    | 13 +---
 .../iterator/kafka/KafkaPipesIteratorConfig.java   | 14 +---
 .../config/MicrosoftGraphFetcherConfig.java        | 15 +---
 .../pipes/emitter/opensearch/HttpClientConfig.java | 10 ---
 .../opensearch/OpenSearchEmitterConfig.java        | 14 +---
 .../reporter/opensearch/HttpClientConfig.java      | 10 ---
 .../opensearch/OpenSearchReporterConfig.java       | 14 +---
 .../tika-pipes-plugins/tika-pipes-s3/pom.xml       |  5 --
 .../tika/pipes/emitter/s3/S3EmitterConfig.java     | 13 +---
 .../pipes/fetcher/s3/config/S3FetcherConfig.java   | 13 +---
 .../pipes/iterator/s3/S3PipesIteratorConfig.java   | 13 +---
 .../tika-pipes-plugins/tika-pipes-solr/pom.xml     |  4 --
 .../tika/pipes/emitter/solr/SolrEmitterConfig.java | 12 +---
 .../iterator/solr/SolrPipesIteratorConfig.java     | 14 +---
 .../java/org/apache/tika/plugins/PluginJson.java   | 60 ++++++++++++++++
 .../org/apache/tika/plugins/PluginJsonTest.java    | 80 ++++++++++++++++++++++
 47 files changed, 269 insertions(+), 425 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 7764f16272..9b7edca602 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,15 @@
 Release 4.1.0 - unreleased
 
+   * Pipes plugins no longer bundle their own Jackson: jackson-core, -databind
+     and -annotations are provided by the host (tika-serialization) and the
+     plugins parent pom now bans bundling them, so a mapper can cross the
+     plugin boundary without a second copy of the Jackson classes (seven plugin
+     zips shipped one). Plugin configuration JSON is parsed by one shared
+     mapper, PluginJson (tika-plugins-core), which rejects unknown keys,
+     numbers for enums and duplicate keys, and accepts
+     // and /* */ comments; the 33 per-plugin *Config classes use it instead of
+     their own bare ObjectMapper (TIKA-4840).
+
    * tika-server and tika-async-cli now start from a config that contains
      // or /* */ comments, as the configuration docs have always said they
      may. The main loader accepted them; the steps that re-read the user's
diff --git a/docs/modules/ROOT/pages/pipes/plugins/writing-a-plugin.adoc 
b/docs/modules/ROOT/pages/pipes/plugins/writing-a-plugin.adoc
index 3bc6f3299a..03f28cf4f4 100644
--- a/docs/modules/ROOT/pages/pipes/plugins/writing-a-plugin.adoc
+++ b/docs/modules/ROOT/pages/pipes/plugins/writing-a-plugin.adoc
@@ -124,8 +124,12 @@ public class MyFetcher extends AbstractTikaExtension 
implements Fetcher {
     }
 }
 ----
-<1> Config arrives as a **JSON string**, and the plugin parses it with its own 
Jackson. Nothing
-richer crosses the boundary — see <<classloading>>.
+<1> Config arrives as a **JSON string**; nothing richer crosses the boundary — 
see
+<<classloading>>. The in-tree plugins parse it with 
`org.apache.tika.plugins.PluginJson`
+(`PluginJson.read(json, MyFetcherConfig.class)`), the host's strict mapper: 
comments allowed,
+unknown keys, duplicate keys and numbers-for-enums rejected. Using it is 
optional — the
+boundary is the `Fetcher`, not how it is configured — but see the Jackson rule 
under
+<<classloading>> before deciding.
 
 IMPORTANT: `Fetcher` implementations must be thread-safe. One instance serves 
every concurrent
 request against that fetcher id.
@@ -275,6 +279,20 @@ of a plugin that loads cleanly and then behaves as if it 
were not there — or o
 The same reasoning covers logging: leave `org.slf4j` and 
`org.apache.logging.log4j` to the host so
 plugin logs land in the host's configuration.
 
+Jackson is the one library with a choice, and it is either/or:
+
+* **Host Jackson (the default, what the in-tree plugins do):** `jackson-core`, 
`jackson-databind`
+  and `jackson-annotations` `provided`, absent from `lib/`. Every Tika host 
carries them (via
+  `tika-serialization`), and you may then use `PluginJson` and other host 
Jackson types.
+* **Your own Jackson:** only if a library you depend on needs a version the 
host does not ship.
+  Bundle it (`compile` scope), and then never touch a host Jackson object — 
not `PluginJson`, not
+  `JsonMetadataList`, nothing returning an `ObjectMapper` or `JsonNode`. Your 
`ObjectMapper` and
+  the host's are different `Class` objects, and the first assignment between 
them fails with a
+  `LinkageError` or `ClassCastException`. Parse `ExtensionConfig.json()` with 
your own mapper.
+
+Never both. The in-tree plugins' parent pom enforces the first choice; a 
third-party plugin
+choosing the second must not inherit that rule.
+
 Everything else — your own transitive libraries — belongs in `lib/`.
 
 == Installing and configuring
@@ -367,6 +385,8 @@ java -Dtika.plugin.dev.mode=true ...
 * `tika-core`, `tika-pipes-api`, `tika-plugins-core`, `tika-serialization`,
   `tika-pipes-core`, `tika-pipes-iterator-commons`, `pf4j` and the logging
   implementations `provided`, and absent from `lib/`.
+* Jackson either `provided` (and then `PluginJson` is yours to use) or bundled 
(and then no host
+  Jackson type is) — never a mix.
 * The zip — not an unpacked directory — dropped in a `plugin-roots` directory.
 * `getName()` unique against every other loaded plugin.
 
diff --git a/tika-pipes/tika-pipes-plugins/pom.xml 
b/tika-pipes/tika-pipes-plugins/pom.xml
index cb51738f24..187bf0e92d 100644
--- a/tika-pipes/tika-pipes-plugins/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/pom.xml
@@ -88,6 +88,23 @@
       <version>${project.version}</version>
       <scope>provided</scope>
     </dependency>
+    <!-- host-supplied via tika-serialization: a plugin that bundles its own 
Jackson gets a
+         second ObjectMapper class, and any mapper crossing the plugin 
boundary fails to link -->
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-core</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <!-- logging is host-supplied -->
     <dependency>
       <groupId>org.slf4j</groupId>
@@ -169,6 +186,7 @@
                   <excludes>
                     <exclude>org.slf4j</exclude>
                     <exclude>org.apache.logging.log4j</exclude>
+                    <exclude>com.fasterxml.jackson.core</exclude>
                   </excludes>
                   <!-- the artifacts this module declares provided; anything 
else in these
                        groups needs a provided declaration here before it may 
be depended on -->
@@ -178,8 +196,11 @@
                     <include>org.apache.logging.log4j:log4j-api</include>
                     <include>org.apache.logging.log4j:log4j-core</include>
                     
<include>org.apache.logging.log4j:log4j-slf4j2-impl</include>
+                    <include>com.fasterxml.jackson.core:jackson-core</include>
+                    
<include>com.fasterxml.jackson.core:jackson-databind</include>
+                    
<include>com.fasterxml.jackson.core:jackson-annotations</include>
                   </includes>
-                  <message>logging is host-supplied: add a provided-scope 
declaration in tika-pipes-plugins/pom.xml (and to this rule's include list) 
rather than bundling this artifact in the plugin zip</message>
+                  <message>logging and Jackson are host-supplied: add a 
provided-scope declaration in tika-pipes-plugins/pom.xml (and to this rule's 
include list) rather than bundling this artifact in the plugin zip. A plugin 
whose SDK genuinely needs its own Jackson may bundle it (compile scope, skip 
this execution in its pom) but must then use no host Jackson type -- not 
PluginJson -- since the two ObjectMapper classes cannot be assigned to each 
other.</message>
                 </bannedDependencies>
               </rules>
               <fail>true</fail>
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/pom.xml 
b/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/pom.xml
index 86b356151e..ffa2818d2e 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/pom.xml
@@ -53,14 +53,6 @@
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-annotations</artifactId>
-    </dependency>
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/src/main/java/org/apache/tika/pipes/fetcher/atlassianjwt/config/AtlassianJwtFetcherConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/src/main/java/org/apache/tika/pipes/fetcher/atlassianjwt/config/AtlassianJwtFetcherConfig.java
index 452d6fc53c..a01ce7d5c8 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/src/main/java/org/apache/tika/pipes/fetcher/atlassianjwt/config/AtlassianJwtFetcherConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-atlassian-jwt/src/main/java/org/apache/tika/pipes/fetcher/atlassianjwt/config/AtlassianJwtFetcherConfig.java
@@ -21,23 +21,14 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public class AtlassianJwtFetcherConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static AtlassianJwtFetcherConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, 
AtlassianJwtFetcherConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse AtlassianJwtFetcherConfig from JSON", e);
-        }
+        return PluginJson.read(json, AtlassianJwtFetcherConfig.class);
     }
 
     private Integer maxConnectionsPerRoute = 1000;
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/pom.xml 
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/pom.xml
index 52d1dd10a5..d2cfbe78ea 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/pom.xml
@@ -46,10 +46,6 @@
       <groupId>com.azure</groupId>
       <artifactId>azure-storage-blob</artifactId>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-    </dependency>
   </dependencies>
 
   <build>
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/emitter/azblob/AZBlobEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/emitter/azblob/AZBlobEmitterConfig.java
index e200a8cc44..d7cfdecaa0 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/emitter/azblob/AZBlobEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/emitter/azblob/AZBlobEmitterConfig.java
@@ -17,10 +17,9 @@
 package org.apache.tika.pipes.emitter.azblob;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record AZBlobEmitterConfig(
         String sasToken,
@@ -31,16 +30,9 @@ public record AZBlobEmitterConfig(
         @JsonProperty(defaultValue = "false") boolean overwriteExisting
 ) {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static AZBlobEmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, AZBlobEmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse AZBlobEmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, AZBlobEmitterConfig.class);
     }
 
     public void validate() throws TikaConfigException {
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/fetcher/azblob/config/AZBlobFetcherConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/fetcher/azblob/config/AZBlobFetcherConfig.java
index d62a512a05..3c715ea82a 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/fetcher/azblob/config/AZBlobFetcherConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/fetcher/azblob/config/AZBlobFetcherConfig.java
@@ -16,23 +16,14 @@
  */
 package org.apache.tika.pipes.fetcher.azblob.config;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public class AZBlobFetcherConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static AZBlobFetcherConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, AZBlobFetcherConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse AZBlobFetcherConfig from JSON", e);
-        }
+        return PluginJson.read(json, AZBlobFetcherConfig.class);
     }
 
     private boolean spoolToTemp = true;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/iterator/azblob/AZBlobPipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/iterator/azblob/AZBlobPipesIteratorConfig.java
index ef3d78a49a..ff91674740 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/iterator/azblob/AZBlobPipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-az-blob/src/main/java/org/apache/tika/pipes/iterator/azblob/AZBlobPipesIteratorConfig.java
@@ -18,25 +18,15 @@ package org.apache.tika.pipes.iterator.azblob;
 
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class AZBlobPipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static AZBlobPipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    AZBlobPipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse AZBlobPipesIteratorConfig from JSON", e);
-        }
+        return PluginJson.read(json, AZBlobPipesIteratorConfig.class);
     }
 
     private String sasToken;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-csv/src/main/java/org/apache/tika/pipes/iterator/csv/CSVPipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-csv/src/main/java/org/apache/tika/pipes/iterator/csv/CSVPipesIteratorConfig.java
index 3a5231821d..2e963799a2 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-csv/src/main/java/org/apache/tika/pipes/iterator/csv/CSVPipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-csv/src/main/java/org/apache/tika/pipes/iterator/csv/CSVPipesIteratorConfig.java
@@ -19,25 +19,15 @@ package org.apache.tika.pipes.iterator.csv;
 import java.nio.file.Path;
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class CSVPipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static CSVPipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    CSVPipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse CSVPipesIteratorConfig from JSON", e);
-        }
+        return PluginJson.read(json, CSVPipesIteratorConfig.class);
     }
 
     private Path csvPath;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-es/src/main/java/org/apache/tika/pipes/emitter/es/ESEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-es/src/main/java/org/apache/tika/pipes/emitter/es/ESEmitterConfig.java
index 288098957b..a68eb7c4cf 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-es/src/main/java/org/apache/tika/pipes/emitter/es/ESEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-es/src/main/java/org/apache/tika/pipes/emitter/es/ESEmitterConfig.java
@@ -16,10 +16,8 @@
  */
 package org.apache.tika.pipes.emitter.es;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 /**
  * Configuration for the ES emitter.
@@ -49,16 +47,9 @@ public record ESEmitterConfig(String esUrl, String idField,
         OVERWRITE, UPSERT
     }
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static ESEmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, ESEmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse ESEmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, ESEmitterConfig.class);
     }
 
     /** Overrides the record default to prevent {@code apiKey} leaking into 
logs. */
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-es/src/main/java/org/apache/tika/pipes/reporter/es/ESReporterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-es/src/main/java/org/apache/tika/pipes/reporter/es/ESReporterConfig.java
index 023f868df1..1507f5aeeb 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-es/src/main/java/org/apache/tika/pipes/reporter/es/ESReporterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-es/src/main/java/org/apache/tika/pipes/reporter/es/ESReporterConfig.java
@@ -18,28 +18,18 @@ package org.apache.tika.pipes.reporter.es;
 
 import java.util.Set;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.metadata.ReservedNamespaces;
 import org.apache.tika.pipes.emitter.es.HttpClientConfig;
+import org.apache.tika.plugins.PluginJson;
 import org.apache.tika.utils.StringUtils;
 
 public record ESReporterConfig(String esUrl, Set<String> includes, Set<String> 
excludes,
                                String keyPrefix, boolean includeRouting,
                                String apiKey, HttpClientConfig 
httpClientConfig) {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static ESReporterConfig load(final String json) throws 
TikaConfigException {
-        ESReporterConfig config;
-        try {
-            config = OBJECT_MAPPER.readValue(json, ESReporterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse ESReporterConfig from JSON", e);
-        }
+        ESReporterConfig config = PluginJson.read(json, 
ESReporterConfig.class);
         // keyPrefix is prepended to this reporter's own scratch-Metadata keys 
(parse_status/
         // parse_time_ms/exit_value); reject a reserved prefix here, before it 
fails every report() call.
         if (!StringUtils.isBlank(config.keyPrefix()) && 
ReservedNamespaces.isTikaNative(config.keyPrefix())) {
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/pom.xml 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/pom.xml
index fde5070d52..06d1602e00 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/pom.xml
@@ -41,11 +41,6 @@
       <version>${project.version}</version>
       <scope>provided</scope>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-      <scope>provided</scope>
-    </dependency>
     <dependency>
       <groupId>com.fasterxml.jackson.datatype</groupId>
       <artifactId>jackson-datatype-jsr310</artifactId>
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
index 54c6530685..7fc17e2d84 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
@@ -16,10 +16,8 @@
  */
 package org.apache.tika.pipes.emitter.fs;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record FileSystemEmitterConfig(String basePath, String fileExtension, 
ON_EXISTS onExists, boolean prettyPrint, boolean allowAbsolutePaths) {
 
@@ -34,17 +32,9 @@ public record FileSystemEmitterConfig(String basePath, 
String fileExtension, ON_
         }
     }
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static FileSystemEmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    FileSystemEmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse FileSystemEmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, FileSystemEmitterConfig.class);
     }
 
 }
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfig.java
index 111f236882..2ceffcea01 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfig.java
@@ -16,10 +16,8 @@
  */
 package org.apache.tika.pipes.emitter.fs;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 /**
  * Runtime configuration for FileSystemEmitter.
@@ -28,17 +26,9 @@ import org.apache.tika.exception.TikaConfigException;
  */
 public class FileSystemEmitterRuntimeConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static FileSystemEmitterRuntimeConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    FileSystemEmitterRuntimeConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse FileSystemEmitterRuntimeConfig from 
JSON", e);
-        }
+        return PluginJson.read(json, FileSystemEmitterRuntimeConfig.class);
     }
 
     private String fileExtension;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherConfig.java
index 7ee64e38d9..6abb55f547 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherConfig.java
@@ -16,24 +16,14 @@
  */
 package org.apache.tika.pipes.fetcher.fs;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public class FileSystemFetcherConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static FileSystemFetcherConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    FileSystemFetcherConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse FileSystemFetcherConfig from JSON", e);
-        }
+        return PluginJson.read(json, FileSystemFetcherConfig.class);
     }
 
     private String basePath;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/iterator/fs/FileSystemPipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/iterator/fs/FileSystemPipesIteratorConfig.java
index 61eeeb66a6..7756fccf85 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/iterator/fs/FileSystemPipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/iterator/fs/FileSystemPipesIteratorConfig.java
@@ -19,26 +19,15 @@ package org.apache.tika.pipes.iterator.fs;
 import java.nio.file.Path;
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class FileSystemPipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static FileSystemPipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    FileSystemPipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse FileSystemPipesIteratorConfig from JSON",
-                    e);
-        }
+        return PluginJson.read(json, FileSystemPipesIteratorConfig.class);
     }
 
     private Path basePath = null;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/reporter/fs/FileSystemReporterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/reporter/fs/FileSystemReporterConfig.java
index 816c8c9c8f..42fe953599 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/reporter/fs/FileSystemReporterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/reporter/fs/FileSystemReporterConfig.java
@@ -19,23 +19,13 @@ package org.apache.tika.pipes.reporter.fs;
 import java.io.Serializable;
 import java.nio.file.Path;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record FileSystemReporterConfig(Path statusFile, long reportUpdateMs) 
implements Serializable {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static FileSystemReporterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    FileSystemReporterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse FileSystemReporterConfig from JSON", e);
-        }
+        return PluginJson.read(json, FileSystemReporterConfig.class);
     }
 }
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherTest.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherTest.java
index 1a30b8c429..eadaa33281 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherTest.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/fetcher/fs/FileSystemFetcherTest.java
@@ -18,6 +18,7 @@ package org.apache.tika.pipes.fetcher.fs;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
 import java.nio.file.Files;
@@ -139,4 +140,16 @@ public class FileSystemFetcherTest {
             assertNotNull(tis);
         }
     }
+
+    @Test
+    public void testUnknownKeyRejected() {
+        // guards the plugin actually parsing through PluginJson, not a 
lenient mapper
+        ObjectNode config = MAPPER.createObjectNode();
+        config.put("basePath", tempDir.toAbsolutePath().toString());
+        config.put("basePaht", "x");
+        ExtensionConfig pluginConfig = new ExtensionConfig("test", "test", 
config.toString());
+        TikaConfigException e = assertThrows(TikaConfigException.class,
+                () -> new 
FileSystemFetcherFactory().buildExtension(pluginConfig));
+        assertTrue(e.getMessage().contains("basePaht"), e.getMessage());
+    }
 }
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitterConfig.java
index 80f2241118..a7408ba4b3 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitterConfig.java
@@ -17,10 +17,9 @@
 package org.apache.tika.pipes.emitter.gcs;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record GCSEmitterConfig(
         String projectId,
@@ -29,16 +28,9 @@ public record GCSEmitterConfig(
         @JsonProperty(defaultValue = "json") String fileExtension
 ) {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static GCSEmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, GCSEmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse GCSEmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, GCSEmitterConfig.class);
     }
 
     public void validate() throws TikaConfigException {
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/config/GCSFetcherConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/config/GCSFetcherConfig.java
index 4c25d37987..d739a07c55 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/config/GCSFetcherConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/config/GCSFetcherConfig.java
@@ -16,23 +16,14 @@
  */
 package org.apache.tika.pipes.fetcher.gcs.config;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public class GCSFetcherConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static GCSFetcherConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, GCSFetcherConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse GCSFetcherConfig from JSON", e);
-        }
+        return PluginJson.read(json, GCSFetcherConfig.class);
     }
 
     private boolean spoolToTemp = true;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/iterator/gcs/GCSPipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/iterator/gcs/GCSPipesIteratorConfig.java
index d87fea102a..9244dd7565 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/iterator/gcs/GCSPipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/iterator/gcs/GCSPipesIteratorConfig.java
@@ -18,24 +18,15 @@ package org.apache.tika.pipes.iterator.gcs;
 
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class GCSPipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static GCSPipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, GCSPipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse GCSPipesIteratorConfig from JSON", e);
-        }
+        return PluginJson.read(json, GCSPipesIteratorConfig.class);
     }
 
     private String bucket;
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/pom.xml 
b/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/pom.xml
index 55483a6f1b..b3926c236f 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/pom.xml
@@ -103,14 +103,6 @@
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-annotations</artifactId>
-    </dependency>
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/src/main/java/org/apache/tika/pipes/fetcher/googledrive/config/GoogleDriveFetcherConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/src/main/java/org/apache/tika/pipes/fetcher/googledrive/config/GoogleDriveFetcherConfig.java
index 1375dda11c..b1dba81253 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/src/main/java/org/apache/tika/pipes/fetcher/googledrive/config/GoogleDriveFetcherConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-google-drive/src/main/java/org/apache/tika/pipes/fetcher/googledrive/config/GoogleDriveFetcherConfig.java
@@ -19,23 +19,14 @@ package org.apache.tika.pipes.fetcher.googledrive.config;
 import java.util.ArrayList;
 import java.util.List;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public class GoogleDriveFetcherConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static GoogleDriveFetcherConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, 
GoogleDriveFetcherConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse GoogleDriveFetcherConfig from JSON", e);
-        }
+        return PluginJson.read(json, GoogleDriveFetcherConfig.class);
     }
 
     private List<Long> throttleSeconds;
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-http/pom.xml 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/pom.xml
index d716a37f9d..92337e4eba 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-http/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-http/pom.xml
@@ -53,14 +53,6 @@
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-annotations</artifactId>
-    </dependency>
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/config/HttpFetcherConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/config/HttpFetcherConfig.java
index 68efc2f69f..bcf5e0a22c 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/config/HttpFetcherConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/config/HttpFetcherConfig.java
@@ -19,23 +19,14 @@ package org.apache.tika.pipes.fetcher.http.config;
 import java.util.ArrayList;
 import java.util.List;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public class HttpFetcherConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static HttpFetcherConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, HttpFetcherConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse HttpFetcherConfig from JSON", e);
-        }
+        return PluginJson.read(json, HttpFetcherConfig.class);
     }
 
     private String userName;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitterConfig.java
index 0b6aeab38d..f8bb48cb5f 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitterConfig.java
@@ -19,10 +19,8 @@ package org.apache.tika.pipes.emitter.jdbc;
 import java.util.LinkedHashMap;
 import java.util.Locale;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record JDBCEmitterConfig(
         String connection,
@@ -59,16 +57,9 @@ public record JDBCEmitterConfig(
         FIRST_ONLY, CONCATENATE
     }
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static JDBCEmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, JDBCEmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse JDBCEmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, JDBCEmitterConfig.class);
     }
 
     public void validate() throws TikaConfigException {
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/iterator/jdbc/JDBCPipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/iterator/jdbc/JDBCPipesIteratorConfig.java
index ff6b68d229..ab92160595 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/iterator/jdbc/JDBCPipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/iterator/jdbc/JDBCPipesIteratorConfig.java
@@ -18,25 +18,15 @@ package org.apache.tika.pipes.iterator.jdbc;
 
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class JDBCPipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static JDBCPipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    JDBCPipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse JDBCPipesIteratorConfig from JSON", e);
-        }
+        return PluginJson.read(json, JDBCPipesIteratorConfig.class);
     }
 
     private String idColumn;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/reporter/jdbc/JDBCPipesReporterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/reporter/jdbc/JDBCPipesReporterConfig.java
index 94a7df3dc1..92ba2d3963 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/reporter/jdbc/JDBCPipesReporterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/reporter/jdbc/JDBCPipesReporterConfig.java
@@ -21,10 +21,9 @@ import java.util.Set;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 /**
  *
@@ -50,17 +49,9 @@ public record JDBCPipesReporterConfig(String 
connectionString, Set<String> inclu
                                       String postConnectionSql,
                                       List<String> reportVariables, long 
reportWithinMs, int cacheSize) {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static JDBCPipesReporterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    JDBCPipesReporterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse JDBCPipesReporterConfig from JSON", e);
-        }
+        return PluginJson.read(json, JDBCPipesReporterConfig.class);
     }
 
     @JsonCreator
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-json/src/main/java/org/apache/tika/pipes/pipesiterator/json/JsonPipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-json/src/main/java/org/apache/tika/pipes/pipesiterator/json/JsonPipesIteratorConfig.java
index c3f6f53924..7574a724a0 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-json/src/main/java/org/apache/tika/pipes/pipesiterator/json/JsonPipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-json/src/main/java/org/apache/tika/pipes/pipesiterator/json/JsonPipesIteratorConfig.java
@@ -19,25 +19,15 @@ package org.apache.tika.pipes.pipesiterator.json;
 import java.nio.file.Path;
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class JsonPipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static JsonPipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    JsonPipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse JsonPipesIteratorConfig from JSON", e);
-        }
+        return PluginJson.read(json, JsonPipesIteratorConfig.class);
     }
 
     private Path jsonPath;
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/pom.xml 
b/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/pom.xml
index 37dd8dcb42..c3f5e729ba 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/pom.xml
@@ -35,11 +35,6 @@
       <version>${project.version}</version>
       <scope>provided</scope>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-      <scope>provided</scope>
-    </dependency>
     <dependency>
       <groupId>org.apache.kafka</groupId>
       <artifactId>kafka-clients</artifactId>
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/src/main/java/org/apache/tika/pipes/emitter/kafka/KafkaEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/src/main/java/org/apache/tika/pipes/emitter/kafka/KafkaEmitterConfig.java
index ba952c91e3..9c34d911bc 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/src/main/java/org/apache/tika/pipes/emitter/kafka/KafkaEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/src/main/java/org/apache/tika/pipes/emitter/kafka/KafkaEmitterConfig.java
@@ -16,10 +16,8 @@
  */
 package org.apache.tika.pipes.emitter.kafka;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 /** Boxed: an unset value stays null so safePut omits the property and Kafka 
applies its own default. */
 public record KafkaEmitterConfig(
@@ -48,16 +46,9 @@ public record KafkaEmitterConfig(
         String valueSerializer
 ) {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static KafkaEmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, KafkaEmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse KafkaEmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, KafkaEmitterConfig.class);
     }
 
     public void validate() throws TikaConfigException {
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/src/main/java/org/apache/tika/pipes/iterator/kafka/KafkaPipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/src/main/java/org/apache/tika/pipes/iterator/kafka/KafkaPipesIteratorConfig.java
index e962a5c568..ff971ea613 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/src/main/java/org/apache/tika/pipes/iterator/kafka/KafkaPipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-kafka/src/main/java/org/apache/tika/pipes/iterator/kafka/KafkaPipesIteratorConfig.java
@@ -18,25 +18,15 @@ package org.apache.tika.pipes.iterator.kafka;
 
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class KafkaPipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static KafkaPipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    KafkaPipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse KafkaPipesIteratorConfig from JSON", e);
-        }
+        return PluginJson.read(json, KafkaPipesIteratorConfig.class);
     }
 
     private String topic;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-microsoft-graph/src/main/java/org/apache/tika/pipes/fetchers/microsoftgraph/config/MicrosoftGraphFetcherConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-microsoft-graph/src/main/java/org/apache/tika/pipes/fetchers/microsoftgraph/config/MicrosoftGraphFetcherConfig.java
index c4e8fd0bda..825632d685 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-microsoft-graph/src/main/java/org/apache/tika/pipes/fetchers/microsoftgraph/config/MicrosoftGraphFetcherConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-microsoft-graph/src/main/java/org/apache/tika/pipes/fetchers/microsoftgraph/config/MicrosoftGraphFetcherConfig.java
@@ -19,25 +19,14 @@ package 
org.apache.tika.pipes.fetchers.microsoftgraph.config;
 import java.util.ArrayList;
 import java.util.List;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public class MicrosoftGraphFetcherConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static MicrosoftGraphFetcherConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    MicrosoftGraphFetcherConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse MicrosoftGraphFetcherConfig from JSON",
-                    e);
-        }
+        return PluginJson.read(json, MicrosoftGraphFetcherConfig.class);
     }
 
     private long[] throttleSeconds;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/emitter/opensearch/HttpClientConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/emitter/opensearch/HttpClientConfig.java
index ddd5e7b5c2..daabfc0208 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/emitter/opensearch/HttpClientConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/emitter/opensearch/HttpClientConfig.java
@@ -16,11 +16,6 @@
  */
 package org.apache.tika.pipes.emitter.opensearch;
 
-import java.io.IOException;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-
 /**
  * @param verifySsl When {@code true} (the default), the HTTP client validates 
server certificates
  *                  and hostnames using the JVM's default trust store.  Set 
{@code false} to trust
@@ -31,11 +26,6 @@ public record HttpClientConfig(String userName, String 
password,
                                String authScheme, int connectionTimeoutMillis, 
int socketTimeoutMillis, String proxyHost, int proxyPort,
                                Boolean verifySsl) {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-    public static HttpClientConfig load(final String json) throws IOException {
-        return OBJECT_MAPPER.readValue(json, HttpClientConfig.class);
-    }
-
     /** Boxed so an absent {@code verifySsl} is distinguishable from an 
explicit {@code false}. */
     public boolean verifySslOrDefault() {
         return verifySsl == null || verifySsl;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/emitter/opensearch/OpenSearchEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/emitter/opensearch/OpenSearchEmitterConfig.java
index 6e4eb095f5..660885d76d 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/emitter/opensearch/OpenSearchEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/emitter/opensearch/OpenSearchEmitterConfig.java
@@ -16,10 +16,8 @@
  */
 package org.apache.tika.pipes.emitter.opensearch;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record OpenSearchEmitterConfig(String openSearchUrl, String idField, 
AttachmentStrategy attachmentStrategy,
                                       UpdateStrategy updateStrategy, int 
commitWithin,
@@ -32,17 +30,9 @@ public record OpenSearchEmitterConfig(String openSearchUrl, 
String idField, Atta
         OVERWRITE, UPSERT
     }
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static OpenSearchEmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    OpenSearchEmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse OpenSearchEmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, OpenSearchEmitterConfig.class);
     }
 
 }
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/reporter/opensearch/HttpClientConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/reporter/opensearch/HttpClientConfig.java
index bed1db04eb..eb20a938a7 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/reporter/opensearch/HttpClientConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/reporter/opensearch/HttpClientConfig.java
@@ -16,11 +16,6 @@
  */
 package org.apache.tika.pipes.reporter.opensearch;
 
-import java.io.IOException;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-
 /**
  * @param verifySsl When {@code true} (the default), the HTTP client validates 
server certificates
  *                  and hostnames using the JVM's default trust store.  Set 
{@code false} to trust
@@ -31,11 +26,6 @@ public record HttpClientConfig(String userName, String 
password,
                                String authScheme, int connectionTimeoutMillis, 
int socketTimeoutMillis, String proxyHost, int proxyPort,
                                Boolean verifySsl) {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-    public static HttpClientConfig load(final String json) throws IOException {
-        return OBJECT_MAPPER.readValue(json, HttpClientConfig.class);
-    }
-
     /** Boxed so an absent {@code verifySsl} is distinguishable from an 
explicit {@code false}. */
     public boolean verifySslOrDefault() {
         return verifySsl == null || verifySsl;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/reporter/opensearch/OpenSearchReporterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/reporter/opensearch/OpenSearchReporterConfig.java
index a787fc3019..1db7abbafc 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/reporter/opensearch/OpenSearchReporterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-opensearch/src/main/java/org/apache/tika/pipes/reporter/opensearch/OpenSearchReporterConfig.java
@@ -18,25 +18,15 @@ package org.apache.tika.pipes.reporter.opensearch;
 
 import java.util.Set;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record OpenSearchReporterConfig(String openSearchUrl, Set<String> 
includes, Set<String> excludes, String keyPrefix,
                                        boolean includeRouting, 
HttpClientConfig httpClientConfig) {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static OpenSearchReporterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    OpenSearchReporterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse OpenSearchReporterConfig from JSON", e);
-        }
+        return PluginJson.read(json, OpenSearchReporterConfig.class);
     }
 
 }
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/pom.xml 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/pom.xml
index e8ffb28c75..8e29cb73a2 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/pom.xml
@@ -41,11 +41,6 @@
       <version>${project.version}</version>
       <scope>provided</scope>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-      <scope>provided</scope>
-    </dependency>
     <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>s3</artifactId>
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfig.java
index e3174aaba4..868f46cc53 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/emitter/s3/S3EmitterConfig.java
@@ -16,10 +16,8 @@
  */
 package org.apache.tika.pipes.emitter.s3;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record S3EmitterConfig(
         String region,
@@ -56,16 +54,9 @@ public record S3EmitterConfig(
         }
     }
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static S3EmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, S3EmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse S3EmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, S3EmitterConfig.class);
     }
 
     public void validate() throws TikaConfigException {
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/config/S3FetcherConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/config/S3FetcherConfig.java
index ff44338428..691b28698b 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/config/S3FetcherConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/fetcher/s3/config/S3FetcherConfig.java
@@ -16,23 +16,14 @@
  */
 package org.apache.tika.pipes.fetcher.s3.config;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public class S3FetcherConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static S3FetcherConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, S3FetcherConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse S3FetcherConfig from JSON", e);
-        }
+        return PluginJson.read(json, S3FetcherConfig.class);
     }
 
     private boolean spoolToTemp = true;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/iterator/s3/S3PipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/iterator/s3/S3PipesIteratorConfig.java
index 4e8cf3ef20..b92a059c1b 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/iterator/s3/S3PipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/org/apache/tika/pipes/iterator/s3/S3PipesIteratorConfig.java
@@ -18,24 +18,15 @@ package org.apache.tika.pipes.iterator.s3;
 
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class S3PipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static S3PipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, S3PipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse S3PipesIteratorConfig from JSON", e);
-        }
+        return PluginJson.read(json, S3PipesIteratorConfig.class);
     }
 
     private String prefix = "";
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/pom.xml 
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/pom.xml
index 62ba948860..8d7f2648fb 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/pom.xml
@@ -67,10 +67,6 @@
       <artifactId>solr-solrj-zookeeper</artifactId>
       <version>${solrj.version}</version>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-    </dependency>
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitterConfig.java
index 8fda64a7b5..5d5b5643b9 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitterConfig.java
@@ -20,10 +20,9 @@ import java.util.List;
 import java.util.Locale;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.plugins.PluginJson;
 
 public record SolrEmitterConfig(
         String solrCollection,
@@ -53,16 +52,9 @@ public record SolrEmitterConfig(
         ADD, UPDATE_MUST_EXIST, UPDATE_MUST_NOT_EXIST
     }
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static SolrEmitterConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json, SolrEmitterConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse SolrEmitterConfig from JSON", e);
-        }
+        return PluginJson.read(json, SolrEmitterConfig.class);
     }
 
     public void validate() throws TikaConfigException {
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIteratorConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIteratorConfig.java
index 0764bf21d8..5043a534a0 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIteratorConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIteratorConfig.java
@@ -20,25 +20,15 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.pipes.pipesiterator.PipesIteratorConfig;
+import org.apache.tika.plugins.PluginJson;
 
 public class SolrPipesIteratorConfig extends PipesIteratorConfig {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
     public static SolrPipesIteratorConfig load(final String json)
             throws TikaConfigException {
-        try {
-            return OBJECT_MAPPER.readValue(json,
-                    SolrPipesIteratorConfig.class);
-        } catch (JsonProcessingException e) {
-            throw new TikaConfigException(
-                    "Failed to parse SolrPipesIteratorConfig from JSON", e);
-        }
+        return PluginJson.read(json, SolrPipesIteratorConfig.class);
     }
 
     private String solrCollection;
diff --git 
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/PluginJson.java 
b/tika-plugins-core/src/main/java/org/apache/tika/plugins/PluginJson.java
new file mode 100644
index 0000000000..03d9093add
--- /dev/null
+++ b/tika-plugins-core/src/main/java/org/apache/tika/plugins/PluginJson.java
@@ -0,0 +1,60 @@
+/*
+ * 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.tika.plugins;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import org.apache.tika.exception.TikaConfigException;
+
+/**
+ * The one {@link ObjectMapper} for plugin configuration JSON ({@link 
ExtensionConfig#json()}).
+ * <p>
+ * Fails fast on what a hand-written config gets wrong: unknown keys, a number 
where an
+ * enum name belongs, duplicate keys. Comments are
+ * accepted, as in the main config loader. Plain JSON only -- no Tika 
component serializers
+ * -- so it is safe to share across the plugin classloader boundary, provided 
the plugin
+ * does not bundle its own Jackson (the plugins parent pom enforces that).
+ */
+public final class PluginJson {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper()
+            .enable(JsonParser.Feature.ALLOW_COMMENTS)
+            // parser-level: FAIL_ON_READING_DUP_TREE_KEY only covers 
readTree, not readValue
+            .enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
+            .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+            // Not FAIL_ON_NULL_FOR_PRIMITIVES: it also fires for a primitive 
record component
+            // that is simply absent, and optional primitives with defaults 
are the norm here.
+            .enable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
+            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+
+    private PluginJson() {
+    }
+
+    /** Deserializes {@code json} into {@code type}, reporting failures as 
config errors. */
+    public static <T> T read(String json, Class<T> type) throws 
TikaConfigException {
+        try {
+            return MAPPER.readValue(json, type);
+        } catch (JsonProcessingException e) {
+            throw new TikaConfigException(
+                    "Failed to parse " + type.getSimpleName() + ": " + 
e.getOriginalMessage(), e);
+        }
+    }
+}
diff --git 
a/tika-plugins-core/src/test/java/org/apache/tika/plugins/PluginJsonTest.java 
b/tika-plugins-core/src/test/java/org/apache/tika/plugins/PluginJsonTest.java
new file mode 100644
index 0000000000..b6be710d2f
--- /dev/null
+++ 
b/tika-plugins-core/src/test/java/org/apache/tika/plugins/PluginJsonTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.tika.plugins;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.exception.TikaConfigException;
+
+public class PluginJsonTest {
+
+    enum Mode { FAST, SLOW }
+
+    public record Sample(String name, long count, Mode mode) { }
+
+    public static class Bean {
+        private int size;
+
+        public int getSize() {
+            return size;
+        }
+
+        public void setSize(int size) {
+            this.size = size;
+        }
+    }
+
+    @Test
+    public void testCommentsAccepted() throws Exception {
+        Sample s = PluginJson.read("""
+                // leading
+                { /* block */ "name": "a", "count": 2, "mode": "FAST" } // 
trailing
+                """, Sample.class);
+        assertEquals(new Sample("a", 2, Mode.FAST), s);
+    }
+
+    @Test
+    public void testRejected() {
+        String[] cases = {
+                "{\"name\":\"a\",\"count\":2,\"mode\":\"FAST\",\"typo\":1}",   
// unknown key
+                "{\"name\":\"a\",\"count\":2,\"mode\":0}",                   
// numeric enum
+                "{\"name\":\"a\",\"count\":2,\"count\":3,\"mode\":\"FAST\"}"  
// duplicate key
+        };
+        for (String json : cases) {
+            TikaConfigException e = assertThrows(TikaConfigException.class,
+                    () -> PluginJson.read(json, Sample.class), json);
+            assertTrue(e.getMessage().startsWith("Failed to parse Sample"), 
e.getMessage());
+        }
+    }
+
+    @Test
+    public void testMissingPrimitiveTakesDefault() throws Exception {
+        // Optional primitives are the norm in plugin configs; absence must 
not be an error.
+        assertEquals(0, PluginJson.read("{\"name\":\"a\",\"mode\":\"SLOW\"}", 
Sample.class).count());
+    }
+
+    @Test
+    public void testDuplicateKeyRejectedForBeans() {
+        // Bean setters would silently take the last value without 
STRICT_DUPLICATE_DETECTION.
+        assertThrows(TikaConfigException.class,
+                () -> PluginJson.read("{\"size\":1,\"size\":2}", Bean.class));
+    }
+}

Reply via email to