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 e2f8e12b4d TIKA-4975 (#3353)
e2f8e12b4d is described below

commit e2f8e12b4d8f9b711cdfa778d96d495d37ecfb5a
Author: Tim Allison <[email protected]>
AuthorDate: Mon Oct 19 21:12:02 2026 -0400

    TIKA-4975 (#3353)
    
    * TIKA-4975: enforce @OperatorOnly wherever a request builds a component
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Co-authored-by: Tilman Hausherr <[email protected]>
---
 .skills/devs/development/SKILL.md                  |  4 ++
 CHANGES.txt                                        | 10 +++++
 .../ROOT/pages/developers/serialization.adoc       |  6 +++
 docs/modules/ROOT/pages/pipes/plugins/http.adoc    | 11 +++++
 .../org/apache/tika/config/ConfigDeserializer.java |  3 ++
 .../apache/tika/pipes/grpc/TikaGrpcServerImpl.java |  2 +
 .../apache/tika/pipes/grpc/TikaGrpcServerTest.java | 15 +++++++
 tika-parent/forbidden-apis-json-reader-bans.txt    | 22 ++++++++++
 tika-parent/pom.xml                                |  3 ++
 .../core/serialization/JsonFetchEmitTupleList.java |  2 +
 .../serialization/JsonFetchEmitTupleListTest.java  | 10 +++++
 .../tika/pipes/emitter/fs/FileSystemEmitter.java   | 11 ++---
 .../emitter/fs/FileSystemEmitterRuntimeConfig.java |  8 ----
 .../fs/FileSystemEmitterRuntimeConfigTest.java     |  5 +--
 .../tika/pipes/fetcher/http/HttpFetcher.java       | 17 ++++----
 .../http/config/HttpFetcherRequestConfig.java      | 26 ++++++-----
 .../tika/pipes/fetcher/http/HttpFetcherTest.java   | 13 +++++-
 .../org/apache/tika/plugins/ExtensionConfig.java   |  7 ++-
 .../java/org/apache/tika/plugins/PluginJson.java   | 48 +++++++++++++--------
 tika-serialization/pom.xml                         |  5 +++
 .../config/loader/TikaObjectMapperFactory.java     | 50 +++++++++++++---------
 .../config/loader/TikaObjectMapperFactoryTest.java | 11 +++++
 .../tika/server/core/resource/TikaResource.java    |  3 +-
 .../core/resource/TikaResourceConfigMergeTest.java | 28 +++++++-----
 24 files changed, 225 insertions(+), 95 deletions(-)

diff --git a/.skills/devs/development/SKILL.md 
b/.skills/devs/development/SKILL.md
index 31407ae236..b02b6f08a2 100644
--- a/.skills/devs/development/SKILL.md
+++ b/.skills/devs/development/SKILL.md
@@ -177,6 +177,10 @@ edits are included.
   invariant, workaround, spec quirk).  Never restate the code, narrate the
   next line, justify the change to a reviewer, or describe past states of
   the code.
+- **A `JsonConfig` is read only through `ConfigDeserializer` or 
`PluginJson.read(JsonConfig, Class)`**,
+  which apply request restrictions and `@OperatorOnly`; forbidden-apis rejects
+  `JsonConfig.json()` in main code outside tika-serialization. Per-request 
plugin config is
+  read into a class holding only what a request may set.
 - **XML is parsed only through `XMLReaderUtils.parseSAX` / `buildDOM`** 
(forbidden-apis
   rejects the JAXP and Commons Secure XML factories and the raw `getSAXParser` 
/
   `getXMLReader` / `getDocumentBuilder` in main code). Never install an 
`EntityResolver`;
diff --git a/CHANGES.txt b/CHANGES.txt
index 3156ae198b..fc0e7f6a6c 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,15 @@
 Release 4.2.0 - unreleased
 
+   * A JsonConfig's raw text is read only through ConfigDeserializer or
+     PluginJson.read(JsonConfig, Class), which apply the request restrictions 
and
+     @OperatorOnly; forbidden-apis rejects JsonConfig.json() elsewhere in 
Tika's main code.
+     Projects that inherit tika-parent inherit the ban; set 
json.forbiddenapis.signaturesFile
+     to forbidden-apis-none.txt to opt out. ExtensionConfig no longer 
implements
+     JsonConfig. A request's http-fetcher config may now set only 
httpRequestHeaders; other
+     keys, previously ignored, are an error. A file-system emitter's runtime 
config that
+     names basePath fails as an unknown key. A duplicate key in configuration 
JSON,
+     previously last-wins, is now an error (TIKA-4975).
+
    * @OperatorOnly is now enforced on every component a request builds, 
wherever
      it is named -- a metadata-filters entry, a nested property, or a bare
      string -- not just on self-configuring parsers. On a class, it keeps a
diff --git a/docs/modules/ROOT/pages/developers/serialization.adoc 
b/docs/modules/ROOT/pages/developers/serialization.adoc
index aad277e085..d9f928ae67 100644
--- a/docs/modules/ROOT/pages/developers/serialization.adoc
+++ b/docs/modules/ROOT/pages/developers/serialization.adoc
@@ -352,6 +352,12 @@ Java code that builds a component from a `JsonConfig` it 
wrote itself is operato
 says so with `JsonConfig.trusted(json)`; a plain lambda is untrusted, so it 
cannot set an
 operator-only property.
 
+A `JsonConfig` may be per-request input, so its raw text is read only by the 
readers that
+apply these checks: `ConfigDeserializer` and the rest of tika-serialization, 
and
+`PluginJson.read(JsonConfig, Class)` for plugins. forbidden-apis rejects 
`JsonConfig.json()`
+anywhere else in Tika's main code. A plugin reads its per-request config into 
a class holding
+only the keys a request may set.
+
 Writing a config class: annotate the setter of anything a caller must not be 
able to point
 elsewhere. A rule the annotation cannot express — a ceiling a request may 
lower but not raise,
 a field that unlocks when the operator opts in — belongs in the component 
after the merge, as
diff --git a/docs/modules/ROOT/pages/pipes/plugins/http.adoc 
b/docs/modules/ROOT/pages/pipes/plugins/http.adoc
index fea9d0e2a7..17e2701934 100644
--- a/docs/modules/ROOT/pages/pipes/plugins/http.adoc
+++ b/docs/modules/ROOT/pages/pipes/plugins/http.adoc
@@ -128,6 +128,17 @@ include::example$pipes-http-fetcher.json[]
 |Base64-encoded private key for asymmetric (RSA/ECDSA) JWT signing. Mutually 
exclusive with `jwtSecret`.
 |===
 
+==== Per-request headers
+
+A request can add headers to one fetch through an `http-fetcher` entry in its 
parse context
+(gRPC: `additional_fetch_config_json`). Only `httpRequestHeaders` may be set 
there; any other
+key is an error, since credentials, proxy, TLS and JWT settings belong to the 
operator.
+
+[source,json]
+----
+{ "httpRequestHeaders": { "map": { "X-Request-Id": ["abc123"] } } }
+----
+
 [#security-notes]
 == Security Notes
 
diff --git 
a/tika-core/src/main/java/org/apache/tika/config/ConfigDeserializer.java 
b/tika-core/src/main/java/org/apache/tika/config/ConfigDeserializer.java
index 18aefa2f16..e28cafd866 100644
--- a/tika-core/src/main/java/org/apache/tika/config/ConfigDeserializer.java
+++ b/tika-core/src/main/java/org/apache/tika/config/ConfigDeserializer.java
@@ -16,6 +16,8 @@
  */
 package org.apache.tika.config;
 
+import org.apache.tika.utils.SuppressForbidden;
+
 /**
  * Deserializes JSON configuration without a compile-time dependency on 
Jackson.
  * <p>
@@ -61,6 +63,7 @@ public class ConfigDeserializer {
      * @throws RuntimeException if tika-serialization is not on the classpath, 
deserialization
      *         fails, or untrusted JSON sets an operator-only property
      */
+    @SuppressForbidden
     public static <T> T buildConfig(JsonConfig jsonConfig, Class<T> 
configClass) {
         ConfigReader reader = ConfigReader.find();
         if (reader == null) {
diff --git 
a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java 
b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
index 7323a01c54..7ce28f672b 100644
--- a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
+++ b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
@@ -24,6 +24,7 @@ import java.util.Objects;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.StreamReadFeature;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.json.JsonMapper;
@@ -85,6 +86,7 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
     private static final Logger LOG = 
LoggerFactory.getLogger(TikaGrpcServerImpl.class);
     private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
             
.defaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL,
 JsonInclude.Include.NON_NULL))
+            .enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION)
             .build();
     public static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new 
JsonSchemaGenerator(OBJECT_MAPPER);
 
diff --git 
a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java 
b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
index 1911cad2f5..8090113fff 100644
--- a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
+++ b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
@@ -568,6 +568,21 @@ public class TikaGrpcServerTest {
                     "the caller must be told which entry: " + 
ex.getStatus().getDescription());
         }
 
+        @Test
+        public void testDuplicateParseContextKeyIsRejected() throws Exception {
+            StatusRuntimeException ex = 
Assertions.assertThrows(StatusRuntimeException.class, () ->
+                    blockingStub.fetchAndParse(FetchAndParseRequest
+                            .newBuilder()
+                            .setFetcherId(createFetcherId(1))
+                            .setFetchKey("no-such-file")
+                            
.setParseContextJson("{\"basic-content-handler-factory\":{\"type\":\"XML\"},"
+                                    + 
"\"basic-content-handler-factory\":{\"type\":\"TEXT\"}}")
+                            .build()));
+            assertEquals(Status.Code.INVALID_ARGUMENT, 
ex.getStatus().getCode());
+            
assertTrue(String.valueOf(ex.getStatus().getDescription()).contains("Duplicate 
field"),
+                    String.valueOf(ex.getStatus().getDescription()));
+        }
+
         /**
          * TIKA-4804: the server-streaming variant must close the call. With 
the in-process
          * transport and a direct executor the whole handler runs inside the 
stub call, so
diff --git a/tika-parent/forbidden-apis-json-reader-bans.txt 
b/tika-parent/forbidden-apis-json-reader-bans.txt
new file mode 100644
index 0000000000..0231dcfaf2
--- /dev/null
+++ b/tika-parent/forbidden-apis-json-reader-bans.txt
@@ -0,0 +1,22 @@
+# 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.
+
+# TIKA-4975: a JsonConfig may be per-request input. Its raw text is read only 
by the
+# readers that apply the request restrictions and @OperatorOnly 
(tika-serialization,
+# ConfigDeserializer, PluginJson); reading it with any other mapper bypasses 
them.
+
+@defaultMessage read a JsonConfig through ConfigDeserializer or 
PluginJson.read(JsonConfig, Class)
+
+org.apache.tika.config.JsonConfig#json()
diff --git a/tika-parent/pom.xml b/tika-parent/pom.xml
index 518889d3d9..2acfabe790 100644
--- a/tika-parent/pom.xml
+++ b/tika-parent/pom.xml
@@ -326,6 +326,8 @@
     <!-- TIKA-4848: default-on in every module's main sources (see 
forbiddenapis-check below) -->
     
<exception.forbiddenapis.signaturesFile>${maven.multiModuleProjectDirectory}/tika-parent/forbidden-apis-exception-reporting-bans.txt</exception.forbiddenapis.signaturesFile>
     
<xml.forbiddenapis.signaturesFile>${maven.multiModuleProjectDirectory}/tika-parent/forbidden-apis-xml-factory-bans.txt</xml.forbiddenapis.signaturesFile>
+    <!-- TIKA-4975: default-on; configuration JSON only through 
tika-serialization -->
+    
<json.forbiddenapis.signaturesFile>${maven.multiModuleProjectDirectory}/tika-parent/forbidden-apis-json-reader-bans.txt</json.forbiddenapis.signaturesFile>
     <!-- Per-module extra main-source bans; a module overrides this (e.g. to
          ${metadata.forbiddenapis.signaturesFile}) instead of overriding the
          forbiddenapis-check execution, whose child list would REPLACE the 
parent's
@@ -1486,6 +1488,7 @@
               <signaturesFiles>
                 
<signaturesFile>${exception.forbiddenapis.signaturesFile}</signaturesFile>
                 
<signaturesFile>${xml.forbiddenapis.signaturesFile}</signaturesFile>
+                
<signaturesFile>${json.forbiddenapis.signaturesFile}</signaturesFile>
                 
<signaturesFile>${forbiddenapis.module.signaturesFile}</signaturesFile>
               </signaturesFiles>
             </configuration>
diff --git 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleList.java
 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleList.java
index 060e75861e..a5948b8ef4 100644
--- 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleList.java
+++ 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleList.java
@@ -22,6 +22,7 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.util.List;
 
+import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -50,6 +51,7 @@ public class JsonFetchEmitTupleList {
         module.addSerializer(ParseContext.class, new ParseContextSerializer());
         module.addDeserializer(ParseContext.class, new 
ParseContextDeserializer());
         OBJECT_MAPPER.registerModule(module);
+        OBJECT_MAPPER.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);
     }
 
     public static List<FetchEmitTuple> fromJson(Reader reader) throws 
IOException {
diff --git 
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleListTest.java
 
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleListTest.java
index 8290637dab..6beebd3e3b 100644
--- 
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleListTest.java
+++ 
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleListTest.java
@@ -65,6 +65,16 @@ public class JsonFetchEmitTupleListTest {
         assertTrue(e.getMessage().contains(JsonFetchEmitTupleList.TUPLES), 
e.getMessage());
     }
 
+    @Test
+    public void testDuplicateKeyIsRejected() {
+        // single-tuple /pipes rejects this; a batch must too, not keep the 
last value
+        String body = 
"{\"tuples\":[{\"id\":\"x\",\"fetcher\":\"f\",\"fetchKey\":\"a\"," +
+                "\"fetchKey\":\"b\",\"emitter\":\"e\"}]}";
+        IOException e = assertThrows(IOException.class,
+                () -> JsonFetchEmitTupleList.fromJson(new StringReader(body)));
+        assertTrue(e.getMessage().contains("fetchKey"), e.getMessage());
+    }
+
     /** Unknown tuple fields must surface as IOException, not Jackson's 
IllegalArgumentException. */
     @Test
     public void testUnknownTupleFieldIsRejectedAsIOException() {
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
index 5e12739827..c5e3621113 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
@@ -40,6 +40,7 @@ import org.apache.tika.parser.ParseContext;
 import org.apache.tika.pipes.api.ParseMode;
 import org.apache.tika.pipes.api.emitter.AbstractStreamEmitter;
 import org.apache.tika.plugins.ExtensionConfig;
+import org.apache.tika.plugins.PluginJson;
 import org.apache.tika.serialization.JsonMetadataList;
 import org.apache.tika.utils.StringUtils;
 
@@ -274,13 +275,9 @@ public class FileSystemEmitter extends 
AbstractStreamEmitter {
         if (parseContext.hasJsonConfig(configKey)) {
             JsonConfig configJson = parseContext.getJsonConfig(configKey);
             if (configJson != null) {
-                // Check if basePath is present in runtime config - this is 
not allowed for security
-                if (configJson.json().contains("\"basePath\"")) {
-                    throw new TikaConfigException("Cannot change 'basePath' at 
runtime for security reasons. " + "basePath can only be set during 
initialization.");
-                }
-
-                // Load runtime config (excludes basePath for security)
-                FileSystemEmitterRuntimeConfig runtimeConfig = 
FileSystemEmitterRuntimeConfig.load(configJson.json());
+                // basePath, allowAbsolutePaths and atomicWrites are not 
runtime keys; naming one fails
+                FileSystemEmitterRuntimeConfig runtimeConfig =
+                        PluginJson.read(configJson, 
FileSystemEmitterRuntimeConfig.class);
 
                 // Merge runtime config into default config while preserving 
basePath and the
                 // init-time allowAbsolutePaths -- neither may be changed at 
runtime.
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 2ceffcea01..45951049fc 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,9 +16,6 @@
  */
 package org.apache.tika.pipes.emitter.fs;
 
-import org.apache.tika.exception.TikaConfigException;
-import org.apache.tika.plugins.PluginJson;
-
 /**
  * Runtime configuration for FileSystemEmitter.
  * Only includes fields that are safe to update at runtime.
@@ -26,11 +23,6 @@ import org.apache.tika.plugins.PluginJson;
  */
 public class FileSystemEmitterRuntimeConfig {
 
-    public static FileSystemEmitterRuntimeConfig load(final String json)
-            throws TikaConfigException {
-        return PluginJson.read(json, FileSystemEmitterRuntimeConfig.class);
-    }
-
     private String fileExtension;
     private FileSystemEmitterConfig.ON_EXISTS onExists;
     private boolean prettyPrint;
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfigTest.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfigTest.java
index f3abd3dd67..d644be393b 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfigTest.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfigTest.java
@@ -71,8 +71,7 @@ public class FileSystemEmitterRuntimeConfigTest {
         Path output1 = dir1.resolve("test1.json");
         assertTrue(Files.exists(output1), "File should be created in dir1");
 
-        // Try to override basePath at runtime to point to dir2
-        // This should throw an exception for security reasons
+        // basePath is not a runtime key, so naming it fails
         String runtimeConfig = String.format(Locale.ROOT,
                 "{\"basePath\":\"%s\", \"onExists\":\"REPLACE\"}",
                 dir2.toString().replace("\\", "\\\\"));
@@ -90,7 +89,7 @@ public class FileSystemEmitterRuntimeConfigTest {
             emitter.emit("test2.json", metadataList2, context2);
         });
         assertTrue(exception.getCause() != null &&
-                exception.getCause().getMessage().contains("Cannot change 
'basePath' at runtime"),
+                exception.getCause().getMessage().contains("basePath"),
                 "Should throw exception when attempting to change basePath at 
runtime");
     }
 
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
index 9d4cffcc54..68e2535d2b 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
@@ -39,8 +39,6 @@ import java.util.Timer;
 import java.util.TimerTask;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.JOSEException;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
@@ -74,11 +72,13 @@ import org.apache.tika.parser.ParseContext;
 import org.apache.tika.pipes.api.fetcher.Fetcher;
 import org.apache.tika.pipes.api.fetcher.RangeFetcher;
 import org.apache.tika.pipes.fetcher.http.config.HttpFetcherConfig;
+import org.apache.tika.pipes.fetcher.http.config.HttpFetcherRequestConfig;
 import org.apache.tika.pipes.fetcher.http.jwt.JwtGenerator;
 import org.apache.tika.pipes.fetcher.http.jwt.JwtPrivateKeyCreds;
 import org.apache.tika.pipes.fetcher.http.jwt.JwtSecretCreds;
 import org.apache.tika.plugins.AbstractTikaExtension;
 import org.apache.tika.plugins.ExtensionConfig;
+import org.apache.tika.plugins.PluginJson;
 import org.apache.tika.utils.StringUtils;
 
 /**
@@ -94,7 +94,6 @@ public class HttpFetcher extends AbstractTikaExtension 
implements Fetcher, Range
     }
 
 
-    private static final ObjectMapper OM = new ObjectMapper();
     private HttpFetcherConfig httpFetcherConfig = new HttpFetcherConfig();
     private HttpClientFactory httpClientFactory = new HttpClientFactory();
 
@@ -160,7 +159,7 @@ public class HttpFetcher extends AbstractTikaExtension 
implements Fetcher, Range
 
     @Override
     public TikaInputStream fetch(String fetchKey, Metadata metadata, 
ParseContext parseContext) throws IOException, TikaException {
-        HttpFetcherConfig additionalHttpFetcherConfig = 
getAdditionalHttpFetcherConfig(parseContext);
+        HttpFetcherRequestConfig additionalHttpFetcherConfig = 
getAdditionalHttpFetcherConfig(parseContext);
         HttpGet get = new HttpGet(fetchKey);
         get.setConfig(buildRequestConfig());
         setHttpRequestHeaders(metadata, get);
@@ -205,12 +204,12 @@ public class HttpFetcher extends AbstractTikaExtension 
implements Fetcher, Range
         }
     }
 
-    private HttpFetcherConfig getAdditionalHttpFetcherConfig(ParseContext 
parseContext) throws JsonProcessingException {
-        // Check for JSON config in ParseContext (new pattern)
+    private HttpFetcherRequestConfig 
getAdditionalHttpFetcherConfig(ParseContext parseContext)
+            throws TikaConfigException {
         if (parseContext.hasJsonConfig("http-fetcher")) {
             JsonConfig jsonConfig = parseContext.getJsonConfig("http-fetcher");
             if (jsonConfig != null) {
-                return OM.readValue(jsonConfig.json(), 
HttpFetcherConfig.class);
+                return PluginJson.read(jsonConfig, 
HttpFetcherRequestConfig.class);
             }
         }
         return null;
@@ -219,7 +218,7 @@ public class HttpFetcher extends AbstractTikaExtension 
implements Fetcher, Range
     @Override
     public TikaInputStream fetch(String fetchKey, long startRange, long 
endRange, Metadata metadata,
                              ParseContext parseContext) throws IOException, 
TikaException {
-        HttpFetcherConfig additionalHttpFetcherConfig = 
getAdditionalHttpFetcherConfig(parseContext);
+        HttpFetcherRequestConfig additionalHttpFetcherConfig = 
getAdditionalHttpFetcherConfig(parseContext);
         HttpGet get = new HttpGet(fetchKey);
         // Same RequestConfig as the whole-document fetch: maxRedirects must 
bind range fetches too.
         get.setConfig(buildRequestConfig());
@@ -230,7 +229,7 @@ public class HttpFetcher extends AbstractTikaExtension 
implements Fetcher, Range
         return execute(get, metadata, httpClient, true);
     }
 
-    private void putAdditionalHeadersOnRequest(HttpFetcherConfig 
additionalFetcherConfig, HttpGet httpGet) throws TikaException {
+    private void putAdditionalHeadersOnRequest(HttpFetcherRequestConfig 
additionalFetcherConfig, HttpGet httpGet) throws TikaException {
         if (!StringUtils.isBlank(httpFetcherConfig.getUserAgent())) {
             httpGet.setHeader(USER_AGENT, httpFetcherConfig.getUserAgent());
         }
diff --git 
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/config/HttpFetcherRequestConfig.java
similarity index 55%
copy from 
tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java
copy to 
tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/config/HttpFetcherRequestConfig.java
index 52bc5d3c09..04e8b7b655 100644
--- 
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/config/HttpFetcherRequestConfig.java
@@ -14,19 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.tika.plugins;
-
-import java.io.Serializable;
-
-import org.apache.tika.config.JsonConfig;
+package org.apache.tika.pipes.fetcher.http.config;
 
 /**
- * Configuration for a plugin extension.
- *
- * @param id         unique instance identifier
- * @param name       the plugin type name
- * @param json       the raw JSON configuration string for the plugin to parse
+ * What a request may set on the http-fetcher (its {@code "http-fetcher"} 
parse-context entry):
+ * extra headers only. Everything else in {@link HttpFetcherConfig} is 
operator config, and
+ * naming it here is an unknown-key error.
  */
-public record ExtensionConfig(String id, String name, String json) implements 
JsonConfig, Serializable {
+public class HttpFetcherRequestConfig {
+
+    private HttpHeaders httpRequestHeaders = new HttpHeaders();
+
+    public HttpHeaders getHttpRequestHeaders() {
+        return httpRequestHeaders;
+    }
 
+    public HttpFetcherRequestConfig setHttpRequestHeaders(HttpHeaders 
httpRequestHeaders) {
+        this.httpRequestHeaders = httpRequestHeaders;
+        return this;
+    }
 }
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
index fb02a28da3..fde8f185eb 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
@@ -63,6 +63,7 @@ import org.mockito.Mockito;
 import org.apache.tika.TikaTest;
 import org.apache.tika.client.HttpClientFactory;
 import org.apache.tika.config.loader.TikaJsonConfig;
+import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.TemporaryResources;
 import org.apache.tika.metadata.Metadata;
@@ -71,6 +72,7 @@ import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.parser.ParseContext;
 import org.apache.tika.pipes.core.fetcher.FetcherManager;
 import org.apache.tika.pipes.fetcher.http.config.HttpFetcherConfig;
+import org.apache.tika.pipes.fetcher.http.config.HttpFetcherRequestConfig;
 import org.apache.tika.pipes.fetcher.http.config.HttpHeaders;
 import org.apache.tika.pipes.fetcher.http.jwt.JwtGenerator;
 import org.apache.tika.plugins.ExtensionConfig;
@@ -169,6 +171,15 @@ class HttpFetcherTest extends TikaTest {
                 .jwt();
     }
 
+    @Test
+    public void testRequestConfigMaySetOnlyHeaders() {
+        ParseContext parseContext = new ParseContext();
+        parseContext.setJsonConfig("http-fetcher", 
"{\"userName\":\"x\",\"password\":\"y\"}");
+        TikaConfigException e = 
Assertions.assertThrows(TikaConfigException.class,
+                () -> httpFetcher.fetch("http://localhost";, new Metadata(), 
parseContext));
+        Assertions.assertTrue(e.getMessage().contains("userName"), 
e.getMessage());
+    }
+
     @Test
     public void testHttpRequestHeaders() throws Exception {
         HttpClient httpClient = Mockito.mock(HttpClient.class);
@@ -201,7 +212,7 @@ class HttpFetcherTest extends TikaTest {
         Metadata metadata = new Metadata();
         ParseContext parseContext = new ParseContext();
 
-        HttpFetcherConfig additionalHttpFetcherConfig = new 
HttpFetcherConfig();
+        HttpFetcherRequestConfig additionalHttpFetcherConfig = new 
HttpFetcherRequestConfig();
         additionalHttpFetcherConfig.setHttpRequestHeaders(new HttpHeaders());
         HashMap<String, Collection<String>> headersMap = new HashMap<>();
         headersMap.put("fromFetchRequestHeader1", 
List.of("fromFetchRequestValue1"));
diff --git 
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java 
b/tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java
index 52bc5d3c09..4ba4b31dae 100644
--- 
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java
+++ 
b/tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java
@@ -18,15 +18,14 @@ package org.apache.tika.plugins;
 
 import java.io.Serializable;
 
-import org.apache.tika.config.JsonConfig;
-
 /**
- * Configuration for a plugin extension.
+ * Configuration for a plugin extension. Not a {@link 
org.apache.tika.config.JsonConfig}: that
+ * type may carry per-request input, and its raw text is read only under the 
request checks.
  *
  * @param id         unique instance identifier
  * @param name       the plugin type name
  * @param json       the raw JSON configuration string for the plugin to parse
  */
-public record ExtensionConfig(String id, String name, String json) implements 
JsonConfig, Serializable {
+public record ExtensionConfig(String id, String name, String json) implements 
Serializable {
 
 }
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
index 03d9093add..406fe56999 100644
--- 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
@@ -16,34 +16,27 @@
  */
 package org.apache.tika.plugins;
 
-import com.fasterxml.jackson.core.JsonParser;
+import java.io.IOException;
+
 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.config.JsonConfig;
+import org.apache.tika.config.loader.TikaObjectMapperFactory;
 import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.serialization.OperatorOnlyGuard;
+import org.apache.tika.utils.SuppressForbidden;
 
 /**
- * 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).
+ * Reads plugin configuration JSON ({@link ExtensionConfig#json()}) with
+ * {@link TikaObjectMapperFactory#createStrictPlainMapper()}: the main config 
loader's checks,
+ * without 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 static final ObjectMapper MAPPER = 
TikaObjectMapperFactory.createStrictPlainMapper();
 
     private PluginJson() {
     }
@@ -57,4 +50,21 @@ public final class PluginJson {
                     "Failed to parse " + type.getSimpleName() + ": " + 
e.getOriginalMessage(), e);
         }
     }
+
+    /**
+     * As {@link #read(String, Class)}, but untrusted JSON (per-request 
config, see
+     * {@link JsonConfig#trusted()}) may not set an {@link 
org.apache.tika.config.OperatorOnly}
+     * property of {@code type}.
+     */
+    @SuppressForbidden
+    public static <T> T read(JsonConfig config, Class<T> type) throws 
TikaConfigException {
+        if (!config.trusted()) {
+            try {
+                OperatorOnlyGuard.check(MAPPER, type.getSimpleName(), 
config.json(), type);
+            } catch (IOException e) {
+                throw new TikaConfigException(e.getMessage(), e);
+            }
+        }
+        return read(config.json(), type);
+    }
 }
diff --git a/tika-serialization/pom.xml b/tika-serialization/pom.xml
index cfff7ed06b..bb9a0c4e93 100644
--- a/tika-serialization/pom.xml
+++ b/tika-serialization/pom.xml
@@ -34,6 +34,11 @@
   <name>Apache Tika serialization</name>
   <url>https://tika.apache.org</url>
 
+  <properties>
+    <!-- TIKA-4975: this module reads JsonConfigs under the request 
restrictions -->
+    
<json.forbiddenapis.signaturesFile>${maven.multiModuleProjectDirectory}/tika-parent/forbidden-apis-none.txt</json.forbiddenapis.signaturesFile>
+  </properties>
+
   <dependencies>
     <!-- Optional OSGi dependency, used only when running within OSGi -->
     <dependency>
diff --git 
a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
 
b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
index 1977005f7c..6e88ca7c94 100644
--- 
a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
+++ 
b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
@@ -151,30 +151,10 @@ public class TikaObjectMapperFactory {
      * @return configured ObjectMapper
      */
     public static ObjectMapper createMapper(JsonFactory factory) {
-        ObjectMapper mapper = (factory != null) ? new ObjectMapper(factory) : 
new ObjectMapper();
-
-        // Allow comments in JSON config files (// and /* */ style)
-        mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
-
-        // Fail on unknown properties to catch configuration errors early
-        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
true);
-
+        ObjectMapper mapper = strict((factory != null) ? new 
ObjectMapper(factory) : new ObjectMapper());
         // Prevent null values being assigned to primitive fields (int, 
boolean, etc.)
         mapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, 
true);
 
-        // Ensure enums are properly validated (not just numeric values)
-        mapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, 
true);
-
-        // Accept enum values in any case ("no_ocr" as well as "NO_OCR"): 
hand-written
-        // requests and older examples spell them either way
-        mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);
-
-        // Catch duplicate keys in JSON objects
-        mapper.configure(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, 
true);
-
-        // Need to allow creation of classes without setters/getters -- we may 
want to revisit this
-        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
-
         sparseOverlays(mapper);
 
         // Load component registries for name resolution
@@ -190,6 +170,34 @@ public class TikaObjectMapperFactory {
         return mapper;
     }
 
+    /**
+     * A strict mapper without Tika's component serializers, for plugin 
configuration
+     * ({@code PluginJson}). Same checks as {@link #getMapper()} except
+     * {@code FAIL_ON_NULL_FOR_PRIMITIVES}, which also rejects an absent 
primitive record
+     * component, and optional primitives with defaults are the norm in plugin 
configs.
+     */
+    public static ObjectMapper createStrictPlainMapper() {
+        return strict(new ObjectMapper());
+    }
+
+    private static ObjectMapper strict(ObjectMapper mapper) {
+        // Allow comments in JSON config files (// and /* */ style)
+        mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
+        // Duplicate keys: the parser feature covers readValue; the tree 
feature only readTree
+        mapper.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);
+        mapper.configure(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, 
true);
+        // Fail on unknown properties to catch configuration errors early
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
true);
+        // Ensure enums are properly validated (not just numeric values)
+        mapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, 
true);
+        // Accept enum values in any case ("no_ocr" as well as "NO_OCR"): 
hand-written
+        // requests and older examples spell them either way
+        mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);
+        // Need to allow creation of classes without setters/getters -- we may 
want to revisit this
+        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+        return mapper;
+    }
+
     /**
      * Loads component registries for name resolution.
      * Registries are loaded from index files and registered with the 
ComponentNameResolver.
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaObjectMapperFactoryTest.java
 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaObjectMapperFactoryTest.java
index d58e5e2c19..60100f028a 100644
--- 
a/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaObjectMapperFactoryTest.java
+++ 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaObjectMapperFactoryTest.java
@@ -19,6 +19,7 @@ package org.apache.tika.config.loader;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.exc.InvalidFormatException;
 import org.junit.jupiter.api.Test;
@@ -50,4 +51,14 @@ public class TikaObjectMapperFactoryTest {
         assertThrows(InvalidFormatException.class,
                 () -> mapper.readValue("{\"strategy\":\"nope\"}", 
Config.class));
     }
+
+    @Test
+    public void testDuplicateKeyFailsOnReadValue() {
+        // the last value would otherwise win silently
+        String json = "{\"strategy\":\"AUTO\",\"strategy\":\"NO_OCR\"}";
+        assertThrows(JsonParseException.class,
+                () -> TikaObjectMapperFactory.getMapper().readValue(json, 
Config.class));
+        assertThrows(JsonParseException.class,
+                () -> TikaObjectMapperFactory.readConfig(json, Config.class, 
true));
+    }
 }
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
index f905cc15ce..89defdb838 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
@@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
 
+import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import jakarta.ws.rs.BadRequestException;
@@ -264,7 +265,7 @@ public class TikaResource {
      * @throws IOException if parsing fails
      */
     public static void mergeParseContextFromConfig(String configJson, 
ParseContext context) throws IOException, TikaConfigException {
-        ObjectMapper mapper = new ObjectMapper();
+        ObjectMapper mapper = new 
ObjectMapper().enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);
         JsonNode root = mapper.readTree(configJson);
         // Request-supplied config: restrict so it cannot bind wire-blocked 
components.
         ParseContext configuredContext = 
ParseContextDeserializer.readParseContext(root, true);
diff --git 
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/resource/TikaResourceConfigMergeTest.java
similarity index 51%
copy from 
tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java
copy to 
tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/resource/TikaResourceConfigMergeTest.java
index 52bc5d3c09..65e76832df 100644
--- 
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/ExtensionConfig.java
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/resource/TikaResourceConfigMergeTest.java
@@ -14,19 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.tika.plugins;
+package org.apache.tika.server.core.resource;
 
-import java.io.Serializable;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import org.apache.tika.config.JsonConfig;
+import java.io.IOException;
 
-/**
- * Configuration for a plugin extension.
- *
- * @param id         unique instance identifier
- * @param name       the plugin type name
- * @param json       the raw JSON configuration string for the plugin to parse
- */
-public record ExtensionConfig(String id, String name, String json) implements 
JsonConfig, Serializable {
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.parser.ParseContext;
+
+public class TikaResourceConfigMergeTest {
 
+    @Test
+    public void testDuplicateKeyIsRejected() {
+        String json = "{\"basic-content-handler-factory\":{\"type\":\"XML\"},"
+                + "\"basic-content-handler-factory\":{\"type\":\"TEXT\"}}";
+        IOException e = assertThrows(IOException.class,
+                () -> TikaResource.mergeParseContextFromConfig(json, new 
ParseContext()));
+        assertTrue(e.getMessage().contains("Duplicate field"), e.getMessage());
+    }
 }

Reply via email to