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 5d180d2d89 TIKA-5059 (#3570)
5d180d2d89 is described below

commit 5d180d2d89c3f3b6ce1da72f1b7cf68ffc442caa
Author: Tim Allison <[email protected]>
AuthorDate: Mon Nov 9 17:13:14 2026 -0500

    TIKA-5059 (#3570)
    
    * TIKA-5059: warn on parse-context blocks for components that never read 
them; @RequestConfigurable marks the ones that do
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 CHANGES.txt                                        |   6 +
 .../ROOT/pages/advanced/language-detection.adoc    |   9 +-
 docs/modules/ROOT/pages/configuration/index.adoc   |  18 ++-
 .../ROOT/pages/developers/serialization.adoc       |  11 ++
 .../apache/tika/config/RequestConfigurable.java    |  50 +++++++
 .../tika/parser/ocr/tess4j/Tess4JParser.java       |   2 +
 .../apache/tika/parser/vlm/ClaudeVLMParser.java    |   2 +
 .../apache/tika/parser/vlm/GeminiVLMParser.java    |   2 +
 .../apache/tika/parser/vlm/OpenAIVLMParser.java    |   2 +
 .../tika/parser/executable/ExecutableParser.java   |   2 +
 .../org/apache/tika/parser/image/BPGParser.java    |   2 +
 .../org/apache/tika/parser/image/Cr3Parser.java    |   2 +
 .../org/apache/tika/parser/image/HeifParser.java   |   2 +
 .../org/apache/tika/parser/image/JpegParser.java   |   2 +
 .../apache/tika/parser/image/RawTiffParser.java    |   2 +
 .../org/apache/tika/parser/image/TiffParser.java   |   2 +
 .../org/apache/tika/parser/image/WebPParser.java   |   2 +
 .../apache/tika/parser/microsoft/EMFParser.java    |   2 +
 .../apache/tika/parser/microsoft/WMFParser.java    |   2 +
 .../apache/tika/parser/ocr/TesseractOCRParser.java |   2 +
 .../java/org/apache/tika/parser/pdf/PDFParser.java |   2 +
 .../tika/serialization/ComponentNameResolver.java  |  17 +++
 .../tika/serialization/ConfigDeserializer.java     |  48 +++++++
 .../serdes/ParseContextDeserializer.java           |  23 ++++
 .../example/tikatest/CustomReaderTestParser.java   |  55 ++++++++
 .../config/loader/RequestConfigTestParser.java     |  70 ++++++++++
 .../loader/RequestConfigTestPlainSubParser.java    |  26 ++++
 .../config/loader/RequestConfigTestSubParser.java  |  33 +++++
 .../config/loader/UnreadWarningTestParser.java     |  59 +++++++++
 .../serialization/RequestConfigurableTest.java     | 147 +++++++++++++++++++++
 .../TestParseContextSerialization.java             |  12 +-
 .../WireRestrictedParseContextTest.java            |   4 +-
 .../serdes/UnreadBlockWarningTest.java             |  34 +++++
 .../tika/server/standard/TikaResourceTest.java     |  17 +++
 34 files changed, 655 insertions(+), 16 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 3718dad971..630012c002 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -50,6 +50,12 @@ Release 4.3.0 - ???
      BoilerpipeContentHandler (tika-handler-boilerpipe) is deprecated 
(TIKA-5054).
 
   SERVER, PIPES AND OPERATIONS
+   * A parse-context block (config file, preset, tika-server /config, gRPC
+     parse_context_json, pipes FetchEmitTuple) for a parser, detector, 
renderer or
+     translator that never reads one is logged as a warning instead of being 
silently
+     ignored; Tika 5.0 will refuse it. Parsers that read a block carry the new
+     @RequestConfigurable annotation, which a custom parser that reads its own 
block
+     should add; until then it still reads it, and a warning names the class 
(TIKA-5059).
 
    * tika-server sets any field of the config's "server" block from the 
command line
      (--serverSetting <field>=<value>, repeatable) and the environment 
(TIKA_SERVER_SETTING_<FIELD>,
diff --git a/docs/modules/ROOT/pages/advanced/language-detection.adoc 
b/docs/modules/ROOT/pages/advanced/language-detection.adoc
index 29dd60ca25..5cfd77f93b 100644
--- a/docs/modules/ROOT/pages/advanced/language-detection.adoc
+++ b/docs/modules/ROOT/pages/advanced/language-detection.adoc
@@ -143,12 +143,9 @@ threshold is meaningful for a short snippet and a full 
document. Per-class
 === Tuning
 
 `CharSoupLanguageDetector` is registered as
-`@TikaComponent(name = "charsoup-language-detector")` and implements
-`SelfConfiguring`, so it resolves by friendly name in the `parse-context` JSON
-section — see
-xref:migration-to-4x/serialization-4x.adoc#discovering-friendly-names[Discovering
 the friendly name for a component].
-It exposes no config record yet; the programmatic knobs are `setMaxLength(int)`
-for the rolling buffer and `setPriors(Map)` for language priors.
+`@TikaComponent(name = "charsoup-language-detector")`. It exposes no config 
record
+yet, so a `parse-context` block for it has no effect; the programmatic knobs 
are
+`setMaxLength(int)` for the rolling buffer and `setPriors(Map)` for language 
priors.
 
 == Training the model
 
diff --git a/docs/modules/ROOT/pages/configuration/index.adoc 
b/docs/modules/ROOT/pages/configuration/index.adoc
index 8ad2251236..acdce6b8de 100644
--- a/docs/modules/ROOT/pages/configuration/index.adoc
+++ b/docs/modules/ROOT/pages/configuration/index.adoc
@@ -47,7 +47,7 @@ optional; anything you omit uses its defaults.
     "exception-reporting": { /* how much exception detail is reported */ },
     "unpack-config": { /* embedded-byte extraction */ },
     "pages": { /* rendering, OCR policy and page images for every parser that 
renders; see below */ }
-    /* other SelfConfiguring components by component name */
+    /* parsers that read a block of their own, by component name; see below */
   },
   "server": { /* tika-server options: allowPipes, allowPerRequestConfig, cors, 
tlsConfig, ... */ },
   "grpc": { /* tika-grpc options */ },
@@ -83,7 +83,8 @@ produces no error and no effect:
 * the `metadata-list` body — only `maxStringLength`, `maxNestingDepth`, 
`maxNumberLength` and
   `typedValues` are read;
 * the body of a self-configuring component under `parse-context` — it is 
handed to the component
-  untouched;
+  untouched (a block for a component that does not read one has no effect and 
is logged as a
+  warning; see <<parse-context-components>>);
 * `_mime-include` / `_mime-exclude` when the value is neither a JSON array nor 
a string — a bare
   string is read as a one-element list, but any other shape yields an empty 
filter rather than a
   failure;
@@ -94,6 +95,19 @@ produces no error and no effect:
 
 If a setting appears to do nothing, check its spelling against one of those 
four first.
 
+[#parse-context-components]
+=== Parsers that read a `parse-context` block
+
+Every parser is self-configuring, but only these read a block of their own 
under `parse-context`,
+whether from the config file, a preset, a tika-server `/config` request, a gRPC
+`parse_context_json` or a pipes `FetchEmitTuple`: `pdf-parser`, 
`tesseract-ocr-parser`,
+`tess4j-parser`, `claude-vlm-parser`, `gemini-vlm-parser`, 
`openai-vlm-parser`, `emf-parser`,
+`wmf-parser`, `executable-parser`, `jpeg-parser`, `tiff-parser`, 
`raw-tiff-parser`,
+`heif-parser`, `bpg-parser`, `cr3-parser` and `web-p-parser`. A block for any 
other parser,
+detector, renderer or translator has no effect: Tika logs a warning naming the 
component, once
+per component per process, and Tika 5.0 will refuse it. Configure those 
components in their own
+section (`parsers`, `detectors`, ...) instead.
+
 [#typed-values]
 === Typed values in metadata JSON (`typedValues`)
 
diff --git a/docs/modules/ROOT/pages/developers/serialization.adoc 
b/docs/modules/ROOT/pages/developers/serialization.adoc
index 5572324295..9ecffd1c5f 100644
--- a/docs/modules/ROOT/pages/developers/serialization.adoc
+++ b/docs/modules/ROOT/pages/developers/serialization.adoc
@@ -189,6 +189,7 @@ it needs it. `Parser` extends `SelfConfiguring`, so every 
parser is self-configu
 
 [source,java]
 ----
+@RequestConfigurable
 @TikaComponent
 public class PDFParser implements Parser, RenderingParser {
 
@@ -205,6 +206,16 @@ public class PDFParser implements Parser, RenderingParser {
 }
 ----
 
+A component that reads such a block must also carry `@RequestConfigurable`. It 
is not
+inherited: a subclass registered under its own name carries it too if it reads 
that name's block.
+A `parse-context` block for a self-configuring component without it is logged 
as a warning when
+the context is read, because nothing reads it, and Tika 5.0 will refuse it. A 
custom component
+that reads its block without the annotation still gets it, and Tika logs a 
warning, once, naming
+the class; add the annotation before 5.0. (For Tika's own components a missing 
annotation fails
+an assertion on every `ParseContextConfig.getConfig` call, block or not, so 
any test that reaches
+that call catches it; a component that calls it only when a block is present 
is caught only by a
+test that sends one.)
+
 Benefits:
 
 * Per-request configuration via `ParseContext`
diff --git 
a/tika-core/src/main/java/org/apache/tika/config/RequestConfigurable.java 
b/tika-core/src/main/java/org/apache/tika/config/RequestConfigurable.java
new file mode 100644
index 0000000000..5a80c3d473
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/config/RequestConfigurable.java
@@ -0,0 +1,50 @@
+/*
+ * 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.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a {@link SelfConfiguring} component that reads a {@code 
parse-context} block under its
+ * own name through {@link ParseContextConfig#getConfig}.
+ * <p>
+ * Every parser, detector, renderer and translator is self-configuring, but 
most never read
+ * such a block. A {@code parse-context} entry naming a self-configuring 
component without
+ * this annotation (in a tika-server request, a gRPC {@code 
parse_context_json}, a pipes
+ * {@code FetchEmitTuple}, a preset or the config file's {@code 
parse-context}) has no effect
+ * and is logged as a warning; Tika 5.0 will refuse it.
+ * <p>
+ * A component that reads its block must carry this annotation. For Tika's own 
components a
+ * missing annotation fails an assertion on every {@link 
ParseContextConfig#getConfig} call,
+ * block or not, so it surfaces in any {@code -ea} test that reaches that call 
(a component
+ * that calls it only when a block is present needs a test that sends one); 
any other
+ * component reading a block without it is logged as a warning, once.
+ * <p>
+ * Not inherited: a subclass registered under its own name reads that name's 
block only if it
+ * says so, so it carries the annotation itself.
+ *
+ * @since Apache Tika 4.3
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface RequestConfigurable {
+}
diff --git 
a/tika-parsers/tika-parsers-ml/tika-parser-tess4j-module/src/main/java/org/apache/tika/parser/ocr/tess4j/Tess4JParser.java
 
b/tika-parsers/tika-parsers-ml/tika-parser-tess4j-module/src/main/java/org/apache/tika/parser/ocr/tess4j/Tess4JParser.java
index 68b1d64d33..a3db4d0d25 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-parser-tess4j-module/src/main/java/org/apache/tika/parser/ocr/tess4j/Tess4JParser.java
+++ 
b/tika-parsers/tika-parsers-ml/tika-parser-tess4j-module/src/main/java/org/apache/tika/parser/ocr/tess4j/Tess4JParser.java
@@ -47,6 +47,7 @@ import org.apache.tika.config.Initializable;
 import org.apache.tika.config.JsonConfig;
 import org.apache.tika.config.ParseContextConfig;
 import org.apache.tika.config.ParseTimeout;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.exception.TikaTimeoutException;
@@ -87,6 +88,7 @@ import org.apache.tika.utils.StringUtils;
  *
  * @since Apache Tika 4.0
  */
+@RequestConfigurable
 @TikaComponent(name = "tess4j-parser")
 public class Tess4JParser implements Parser, Initializable, TextRecognizer {
 
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/ClaudeVLMParser.java
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/ClaudeVLMParser.java
index d617c7fed0..c2dfcd1369 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/ClaudeVLMParser.java
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/ClaudeVLMParser.java
@@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.metadata.Metadata;
@@ -52,6 +53,7 @@ import org.apache.tika.utils.StringUtils;
  *
  * @since Apache Tika 4.0
  */
+@RequestConfigurable
 @TikaComponent(spi = false)
 public class ClaudeVLMParser extends AbstractVLMParser {
 
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/GeminiVLMParser.java
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/GeminiVLMParser.java
index c7a47c3def..c7072a06e6 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/GeminiVLMParser.java
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/GeminiVLMParser.java
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.metadata.Metadata;
@@ -54,6 +55,7 @@ import org.apache.tika.utils.StringUtils;
  *
  * @since Apache Tika 4.0
  */
+@RequestConfigurable
 @TikaComponent(spi = false)
 public class GeminiVLMParser extends AbstractVLMParser {
 
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/OpenAIVLMParser.java
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/OpenAIVLMParser.java
index bb05d608fc..3e368f86a4 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/OpenAIVLMParser.java
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/main/java/org/apache/tika/parser/vlm/OpenAIVLMParser.java
@@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.mime.MediaType;
@@ -60,6 +61,7 @@ import org.apache.tika.utils.StringUtils;
  * @since Apache Tika 4.0
  */
 // spi = false: VLM parsers are selected by name in config, never 
auto-registered (TIKA-4871)
+@RequestConfigurable
 @TikaComponent(name = "openai-vlm-parser", spi = false)
 public class OpenAIVLMParser extends AbstractVLMParser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/ExecutableParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/ExecutableParser.java
index 9b3b37cdd1..20fc42680f 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/ExecutableParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/ExecutableParser.java
@@ -33,6 +33,7 @@ import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
 import org.apache.tika.config.ParseContextConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.EndianUtils;
 import org.apache.tika.io.TikaInputStream;
@@ -48,6 +49,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
 /**
  * Parser for executable files. Currently supports ELF and PE
  */
+@RequestConfigurable
 @TikaComponent
 public class ExecutableParser implements Parser, MachineMetadata {
     /**
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/BPGParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/BPGParser.java
index 664a288901..d005df9f76 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/BPGParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/BPGParser.java
@@ -30,6 +30,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.exception.TikaMemoryLimitException;
 import org.apache.tika.io.EndianUtils;
@@ -45,6 +46,7 @@ import org.apache.tika.parser.ParseContext;
  * Documentation on the file format is available from
  * http://bellard.org/bpg/bpg_spec.txt
  */
+@RequestConfigurable
 @TikaComponent
 public class BPGParser extends AbstractImageParser {
     protected static final int EXTENSION_TAG_EXIF = 1;
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/Cr3Parser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/Cr3Parser.java
index bb711441e1..e0e0bd498a 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/Cr3Parser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/Cr3Parser.java
@@ -43,6 +43,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.extractor.EmbeddedDocumentExtractor;
 import org.apache.tika.extractor.EmbeddedDocumentUtil;
@@ -76,6 +77,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
  * The {@code THMB} and {@code PRVW} records carry width and height and the 
JPEG
  * length ahead of the JPEG, which starts 24 bytes into the record.
  */
+@RequestConfigurable
 @TikaComponent(name = "cr3-parser")
 public class Cr3Parser implements Parser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/HeifParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/HeifParser.java
index 757c1927c6..6c83371e3b 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/HeifParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/HeifParser.java
@@ -31,6 +31,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.TemporaryResources;
 import org.apache.tika.io.TikaInputStream;
@@ -39,6 +40,7 @@ import org.apache.tika.mime.MediaType;
 import org.apache.tika.parser.ParseContext;
 
 
+@RequestConfigurable
 @TikaComponent
 public class HeifParser extends AbstractImageParser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/JpegParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/JpegParser.java
index 151fa1e287..d93a02c8dc 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/JpegParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/JpegParser.java
@@ -27,6 +27,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.CacheMemoryBudget;
 import org.apache.tika.io.TemporaryResources;
@@ -35,6 +36,7 @@ import org.apache.tika.metadata.Metadata;
 import org.apache.tika.mime.MediaType;
 import org.apache.tika.parser.ParseContext;
 
+@RequestConfigurable
 @TikaComponent
 public class JpegParser extends AbstractImageParser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
index da58f555f7..8806180a51 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
@@ -45,6 +45,7 @@ import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
 import org.apache.tika.config.OperatorOnly;
 import org.apache.tika.config.ParseContextConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.extractor.EmbeddedDocumentExtractor;
 import org.apache.tika.extractor.EmbeddedDocumentUtil;
@@ -90,6 +91,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
  * ones are {@link TikaCoreProperties.EmbeddedResourceType#INLINE} images
  * (TIKA-4851).
  */
+@RequestConfigurable
 @TikaComponent
 public class RawTiffParser extends TiffParser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/TiffParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/TiffParser.java
index 056f36dcae..4debd693e6 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/TiffParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/TiffParser.java
@@ -27,6 +27,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.CacheMemoryBudget;
 import org.apache.tika.io.TemporaryResources;
@@ -35,6 +36,7 @@ import org.apache.tika.metadata.Metadata;
 import org.apache.tika.mime.MediaType;
 import org.apache.tika.parser.ParseContext;
 
+@RequestConfigurable
 @TikaComponent
 public class TiffParser extends AbstractImageParser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/WebPParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/WebPParser.java
index e4890576fa..f1490b5500 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/WebPParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/WebPParser.java
@@ -26,6 +26,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.CacheMemoryBudget;
 import org.apache.tika.io.TikaInputStream;
@@ -35,6 +36,7 @@ import org.apache.tika.parser.ParseContext;
 import org.apache.tika.parser.Parser;
 import org.apache.tika.sax.XHTMLContentHandler;
 
+@RequestConfigurable
 @TikaComponent
 public class WebPParser implements Parser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java
index 09d3bec35e..71061f1487 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java
@@ -37,6 +37,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.extractor.EmbeddedDocumentExtractor;
 import org.apache.tika.extractor.EmbeddedDocumentUtil;
@@ -76,6 +77,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
  * raster preview of a vector thumbnail such as the docProps thumbnail of a
  * Word document.
  */
+@RequestConfigurable
 @TikaComponent
 public class EMFParser extends AbstractMetafileParser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java
index 6a1f2e210d..af926c8e9c 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java
@@ -34,6 +34,7 @@ import org.xml.sax.SAXException;
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Metadata;
@@ -56,6 +57,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
  * {@link TikaCoreProperties.EmbeddedResourceType#RENDERING} embedded document,
  * the way the PDF parser emits page renderings.
  */
+@RequestConfigurable
 @TikaComponent
 public class WMFParser extends AbstractMetafileParser {
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/TesseractOCRParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/TesseractOCRParser.java
index c5855b0cbf..8639b88a8b 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/TesseractOCRParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/TesseractOCRParser.java
@@ -63,6 +63,7 @@ import org.apache.tika.config.Initializable;
 import org.apache.tika.config.JsonConfig;
 import org.apache.tika.config.ParseContextConfig;
 import org.apache.tika.config.ParseTimeout;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.TikaConfigException;
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.exception.TikaTimeoutException;
@@ -96,6 +97,7 @@ import org.apache.tika.utils.XMLReaderUtils;
  * parseContext.set(TesseractOCRConfig.class, config);<br>
  * </p>
  */
+@RequestConfigurable
 // name pinned: the documented "text-recognizers" selector for this engine
 @TikaComponent(name = "tesseract-ocr-parser")
 public class TesseractOCRParser extends AbstractExternalProcessParser
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
index 121f489236..725d420745 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
@@ -59,6 +59,7 @@ import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.config.ConfigDeserializer;
 import org.apache.tika.config.JsonConfig;
 import org.apache.tika.config.ParseContextConfig;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.exception.AccessPermissionException;
 import org.apache.tika.exception.EncryptedDocumentException;
 import org.apache.tika.exception.TikaException;
@@ -124,6 +125,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
  * If your PDFs contain marked content or tags, consider
  * {@link PDFParserConfig#setMarkedContent(MarkedContentConfig)}
  */
+@RequestConfigurable
 @TikaComponent
 public class PDFParser implements Parser, RenderingParser, EnrichingParser {
 
diff --git 
a/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
 
b/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
index af19e4c34f..2a8ff8c4e0 100644
--- 
a/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
+++ 
b/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
@@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.tika.config.ExceptionReporting;
 import org.apache.tika.config.OperatorOnly;
+import org.apache.tika.config.RequestConfigurable;
 import org.apache.tika.config.loader.ComponentInfo;
 import org.apache.tika.config.loader.ComponentRegistry;
 import org.apache.tika.detect.Detector;
@@ -334,6 +335,22 @@ public final class ComponentNameResolver {
         return false;
     }
 
+    /**
+     * False for a self-configuring component that does not read a {@code 
parse-context} block
+     * under its own name (see {@link RequestConfigurable}); true for 
everything else.
+     */
+    public static boolean readsParseContextConfig(ComponentInfo info) {
+        return !info.selfConfiguring()
+                || 
info.componentClass().isAnnotationPresent(RequestConfigurable.class);
+    }
+
+    public static String notRequestConfigurableMessage(String name, Class<?> 
clazz) {
+        return "Component '" + name + "' (" + clazz.getName() + ") does not 
read a per-request "
+                + "or parse-context block, so this block has no effect; Tika 
5.0 will refuse it. "
+                + "Configure the component in the Tika config's component 
section instead. If it "
+                + "is a custom component that reads this block, annotate it 
@RequestConfigurable.";
+    }
+
     /** The refusal for a class {@link #isWireBlockedClass} blocks. */
     public static String wireBlockedClassMessage(String name, Class<?> clazz) {
         return "Component '" + name + "' (" + clazz.getName() + ") may not be 
supplied via a "
diff --git 
a/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
 
b/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
index 75f12777a6..b18c99d450 100644
--- 
a/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
+++ 
b/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
@@ -17,10 +17,17 @@
 package org.apache.tika.serialization;
 
 import java.io.IOException;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.RequestConfigurable;
+import org.apache.tika.config.loader.ComponentInfo;
 import org.apache.tika.config.loader.JsonMergeUtils;
 import org.apache.tika.config.loader.TikaObjectMapperFactory;
 import org.apache.tika.parser.ParseContext;
@@ -52,6 +59,11 @@ import org.apache.tika.parser.ParseContext;
  */
 public class ConfigDeserializer {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(ConfigDeserializer.class);
+
+    // bounded by the component registry
+    private static final Set<String> WARNED_UNDECLARED = 
ConcurrentHashMap.newKeySet();
+
     private static final ObjectMapper MAPPER = 
TikaObjectMapperFactory.getMapper();
 
     /**
@@ -82,6 +94,8 @@ public class ConfigDeserializer {
         if (context == null) {
             return defaultConfig;
         }
+        // every call, block or not, so a Tika reader's own -ea tests catch a 
missing annotation
+        assert declaredOrCustom(configKey) : undeclaredMessage(configKey);
 
         // Check resolved cache first
         T resolved = context.getResolvedConfig(configKey, configClass);
@@ -95,6 +109,7 @@ public class ConfigDeserializer {
             return defaultConfig;
         }
 
+        warnIfUndeclaredCustom(configKey);
         if (!jsonConfig.trusted()) {
             OperatorOnlyGuard.check(MAPPER, configKey, jsonConfig.json(), 
configClass);
         }
@@ -111,6 +126,39 @@ public class ConfigDeserializer {
         return config;
     }
 
+    /**
+     * An undeclared reader gets a false "has no effect" warning. Tika's own 
components fail an
+     * assertion (tests run with -ea); a custom one gets a warning, once, so 
upgrading does not
+     * break its tests.
+     */
+    private static boolean declaredOrCustom(String configKey) {
+        return undeclaredReader(configKey)
+                .map(c -> !c.getName().startsWith("org.apache.tika."))
+                .orElse(true);
+    }
+
+    private static void warnIfUndeclaredCustom(String configKey) {
+        undeclaredReader(configKey).ifPresent(c -> {
+            if (!c.getName().startsWith("org.apache.tika.") && 
WARNED_UNDECLARED.add(c.getName())) {
+                LOG.warn("{}; add the annotation, Tika 5.0 will refuse the 
block otherwise",
+                        undeclaredMessage(configKey));
+            }
+        });
+    }
+
+    /** The component named {@code configKey} if it is self-configuring and 
not annotated. */
+    private static Optional<Class<?>> undeclaredReader(String configKey) {
+        return ComponentNameResolver.getComponentInfo(configKey)
+                .filter(info -> 
!ComponentNameResolver.readsParseContextConfig(info))
+                .map(ComponentInfo::componentClass);
+    }
+
+    private static String undeclaredMessage(String configKey) {
+        return undeclaredReader(configKey).map(Class::getName).orElse("?") + " 
reads its '"
+                + configKey + "' parse-context block but is not annotated @"
+                + RequestConfigurable.class.getSimpleName();
+    }
+
     /**
      * Retrieves and deserializes a configuration from ParseContext.
      * This version does not merge with any default config.
diff --git 
a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
 
b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
index c4b85479e8..98fe4217a4 100644
--- 
a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
+++ 
b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
@@ -23,6 +23,8 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.DeserializationContext;
@@ -46,6 +48,9 @@ public class ParseContextDeserializer extends 
JsonDeserializer<ParseContext> {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(ParseContextDeserializer.class);
 
+    // bounded by the component registry: unregistered names never get here
+    private static final Set<String> WARNED_UNREAD = 
ConcurrentHashMap.newKeySet();
+
     /**
      * When true, refuses context-key types blocked from untrusted wire input
      * (see {@link ComponentNameResolver#isWireBlocked(Class)}). Use for 
request bodies (e.g.
@@ -114,6 +119,7 @@ public class ParseContextDeserializer extends 
JsonDeserializer<ParseContext> {
         while (fieldNames.hasNext()) {
             String name = fieldNames.next();
             JsonNode value = contextNode.get(name);
+            warnIfUnread(name);
             checkForDuplicateContextKey(name, seenContextKeys);
             // Store as a lazy JSON config (plain mapper: the main one may be 
binary/Smile).
             parseContext.setJsonConfig(name, 
plainMapper().writeValueAsString(value), !restricted);
@@ -122,6 +128,23 @@ public class ParseContextDeserializer extends 
JsonDeserializer<ParseContext> {
         return parseContext;
     }
 
+    /**
+     * Warns, once per component, about a block for a self-configuring 
component that never
+     * reads one: it is accepted and has no effect. To be refused in 5.0.
+     *
+     * @return true if this call logged the warning
+     */
+    static boolean warnIfUnread(String name) {
+        Optional<ComponentInfo> infoOpt = 
ComponentNameResolver.getComponentInfo(name);
+        if (infoOpt.isPresent() && 
!ComponentNameResolver.readsParseContextConfig(infoOpt.get())
+                && WARNED_UNREAD.add(name)) {
+            LOG.warn(ComponentNameResolver.notRequestConfigurableMessage(name,
+                    infoOpt.get().componentClass()));
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Checks if a JSON config entry would create a duplicate context key.
      * <p>
diff --git 
a/tika-serialization/src/test/java/com/example/tikatest/CustomReaderTestParser.java
 
b/tika-serialization/src/test/java/com/example/tikatest/CustomReaderTestParser.java
new file mode 100644
index 0000000000..3710decaea
--- /dev/null
+++ 
b/tika-serialization/src/test/java/com/example/tikatest/CustomReaderTestParser.java
@@ -0,0 +1,55 @@
+/*
+ * 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 com.example.tikatest;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ParseContextConfig;
+import org.apache.tika.config.loader.RequestConfigTestParser;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+
+/** A third-party parser that reads its parse-context block but predates 
@RequestConfigurable. */
+@TikaComponent(name = "custom-reader-test-parser", spi = false)
+public class CustomReaderTestParser implements Parser {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public Set<MediaType> getSupportedTypes(ParseContext context) {
+        return 
Collections.singleton(MediaType.parse("application/test+custom-reader"));
+    }
+
+    @Override
+    public void parse(TikaInputStream stream, ContentHandler handler, Metadata 
metadata,
+                      ParseContext context) throws IOException, SAXException, 
TikaException {
+        RequestConfigTestParser.Config config = 
ParseContextConfig.getConfig(context,
+                "custom-reader-test-parser", 
RequestConfigTestParser.Config.class,
+                new RequestConfigTestParser.Config());
+        metadata.set("maxItems", Integer.toString(config.getMaxItems()));
+    }
+}
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestParser.java
 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestParser.java
new file mode 100644
index 0000000000..b8c45f4c49
--- /dev/null
+++ 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestParser.java
@@ -0,0 +1,70 @@
+/*
+ * 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.config.loader;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ParseContextConfig;
+import org.apache.tika.config.RequestConfigurable;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+
+/** Reads a parse-context block under its own name and reports maxItems in the 
metadata. */
+@RequestConfigurable
+@TikaComponent(name = "request-config-test-parser", spi = false)
+public class RequestConfigTestParser implements Parser {
+
+    private static final long serialVersionUID = 1L;
+
+    protected String configKey() {
+        return "request-config-test-parser";
+    }
+
+    @Override
+    public Set<MediaType> getSupportedTypes(ParseContext context) {
+        return 
Collections.singleton(MediaType.parse("application/test+request-config"));
+    }
+
+    @Override
+    public void parse(TikaInputStream stream, ContentHandler handler, Metadata 
metadata,
+                      ParseContext context) throws IOException, SAXException, 
TikaException {
+        Config config = ParseContextConfig.getConfig(context, configKey(), 
Config.class, new Config());
+        metadata.set("maxItems", Integer.toString(config.getMaxItems()));
+    }
+
+    public static class Config {
+        private int maxItems = 1;
+
+        public int getMaxItems() {
+            return maxItems;
+        }
+
+        public void setMaxItems(int maxItems) {
+            this.maxItems = maxItems;
+        }
+    }
+}
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestPlainSubParser.java
 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestPlainSubParser.java
new file mode 100644
index 0000000000..56ff8f17b0
--- /dev/null
+++ 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestPlainSubParser.java
@@ -0,0 +1,26 @@
+/*
+ * 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.config.loader;
+
+import org.apache.tika.annotation.TikaComponent;
+
+/** Keeps its superclass's config key, so a block under its own name is never 
read. */
+@TikaComponent(name = "request-config-test-plain-sub-parser", spi = false)
+public class RequestConfigTestPlainSubParser extends RequestConfigTestParser {
+
+    private static final long serialVersionUID = 1L;
+}
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestSubParser.java
 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestSubParser.java
new file mode 100644
index 0000000000..cb226fce3a
--- /dev/null
+++ 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/RequestConfigTestSubParser.java
@@ -0,0 +1,33 @@
+/*
+ * 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.config.loader;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.RequestConfigurable;
+
+/** Reads a block under its own name, so it carries {@code 
@RequestConfigurable} itself. */
+@RequestConfigurable
+@TikaComponent(name = "request-config-test-sub-parser", spi = false)
+public class RequestConfigTestSubParser extends RequestConfigTestParser {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    protected String configKey() {
+        return "request-config-test-sub-parser";
+    }
+}
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/config/loader/UnreadWarningTestParser.java
 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/UnreadWarningTestParser.java
new file mode 100644
index 0000000000..298ee26a0b
--- /dev/null
+++ 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/UnreadWarningTestParser.java
@@ -0,0 +1,59 @@
+/*
+ * 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.config.loader;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+
+/**
+ * Self-configuring and not {@code @RequestConfigurable}; only 
UnreadBlockWarningTest names it.
+ */
+@TikaComponent(name = "unread-warning-test-parser")
+public class UnreadWarningTestParser implements Parser {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Zero-arg constructor only - no JSON config support.
+     */
+    public UnreadWarningTestParser() {
+        // No config
+    }
+
+    @Override
+    public Set<MediaType> getSupportedTypes(ParseContext context) {
+        return 
Collections.singleton(MediaType.parse("application/test+unread-warning"));
+    }
+
+    @Override
+    public void parse(TikaInputStream stream, ContentHandler handler, Metadata 
metadata,
+                      ParseContext context) throws IOException, SAXException, 
TikaException {
+        metadata.set("parser-type", "unread-warning");
+    }
+}
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/serialization/RequestConfigurableTest.java
 
b/tika-serialization/src/test/java/org/apache/tika/serialization/RequestConfigurableTest.java
new file mode 100644
index 0000000000..e8260dd327
--- /dev/null
+++ 
b/tika-serialization/src/test/java/org/apache/tika/serialization/RequestConfigurableTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import com.example.tikatest.CustomReaderTestParser;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.config.loader.MinimalTestParser;
+import org.apache.tika.config.loader.RequestConfigTestParser;
+import org.apache.tika.config.loader.RequestConfigTestPlainSubParser;
+import org.apache.tika.config.loader.RequestConfigTestSubParser;
+import org.apache.tika.config.loader.TikaLoader;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
+
+/** Which self-configuring components read a parse-context block of their own. 
*/
+public class RequestConfigurableTest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testAcceptsBlockNobodyReads(boolean restricted) throws 
Exception {
+        // warned about, not refused, until 5.0
+        ParseContext context = ParseContextDeserializer.readParseContext(
+                json("{\"minimal-test-parser\":{}}"), restricted);
+        assertTrue(context.hasJsonConfig("minimal-test-parser"));
+    }
+
+    @Test
+    public void testOnlyAnnotatedSelfConfiguringComponentsRead() {
+        assertFalse(readsBlock("minimal-test-parser"));
+        assertTrue(readsBlock("request-config-test-parser"));
+        assertTrue(readsBlock("request-config-test-sub-parser"));
+        // not self-configuring: resolved into the context, so always read
+        assertTrue(readsBlock("timeout-limits"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testAppliesBlockForReader(boolean restricted) throws Exception 
{
+        ParseContext context = ParseContextDeserializer.readParseContext(
+                json("{\"request-config-test-parser\":{\"maxItems\":7}}"), 
restricted);
+        assertEquals("7", parse(new RequestConfigTestParser(), 
context).get("maxItems"));
+    }
+
+    @Test
+    public void testAnnotatedSubclassReadsOwnBlock() throws Exception {
+        ParseContext context = ParseContextDeserializer.readParseContext(
+                json("{\"request-config-test-sub-parser\":{\"maxItems\":3}}"), 
true);
+        assertEquals("3", parse(new RequestConfigTestSubParser(), 
context).get("maxItems"));
+    }
+
+    /** A subclass reading its superclass's key must not inherit the 
annotation and its silence. */
+    @Test
+    public void testAnnotationNotInherited() throws Exception {
+        assertFalse(readsBlock("request-config-test-plain-sub-parser"));
+        ParseContext context = ParseContextDeserializer.readParseContext(
+                
json("{\"request-config-test-plain-sub-parser\":{\"maxItems\":3}}"), true);
+        assertEquals("1", parse(new RequestConfigTestPlainSubParser(), 
context).get("maxItems"));
+    }
+
+    @Test
+    public void testConfigFileParseContextAcceptsBlockNobodyReads(@TempDir 
Path tmp) throws Exception {
+        Path config = Files.writeString(tmp.resolve("tika-config.json"),
+                "{\"parse-context\":{\"minimal-test-parser\":{}}}", 
StandardCharsets.UTF_8);
+        
assertTrue(TikaLoader.load(config).loadParseContext().hasJsonConfig("minimal-test-parser"));
+    }
+
+    @Test
+    public void testReadingWithoutAnnotationFailsUnderAssertions() {
+        assumeTrue(ConfigDeserializer.class.desiredAssertionStatus());
+        ParseContext context = new ParseContext();
+        context.setJsonConfig("minimal-test-parser", "{}");
+        AssertionError e = assertThrows(AssertionError.class,
+                () -> ConfigDeserializer.getConfig(context, 
"minimal-test-parser", MinimalTestParser.class));
+        assertTrue(e.getMessage().contains("RequestConfigurable"), 
e.getMessage());
+    }
+
+    /** Without a block too, so a reader's own tests catch it whether or not 
they send one. */
+    @Test
+    public void testReadingWithoutAnnotationFailsWithNoBlock() {
+        assumeTrue(ConfigDeserializer.class.desiredAssertionStatus());
+        assertThrows(AssertionError.class, () -> 
ConfigDeserializer.getConfig(new ParseContext(),
+                "minimal-test-parser", MinimalTestParser.class, new 
MinimalTestParser()));
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testCustomReaderWithoutAnnotationStillWorks(boolean 
restricted) throws Exception {
+        // only Tika's own components fail the assertion; upgrading must not 
break third-party tests
+        ParseContext context = ParseContextDeserializer.readParseContext(
+                json("{\"custom-reader-test-parser\":{\"maxItems\":4}}"), 
restricted);
+        assertEquals("4", parse(new CustomReaderTestParser(), 
context).get("maxItems"));
+    }
+
+    private static boolean readsBlock(String name) {
+        return ComponentNameResolver.readsParseContextConfig(
+                ComponentNameResolver.getComponentInfo(name).orElseThrow());
+    }
+
+    private static JsonNode json(String s) throws IOException {
+        return MAPPER.readTree(s);
+    }
+
+    private static Metadata parse(Parser parser, ParseContext context) throws 
Exception {
+        Metadata metadata = new Metadata();
+        try (TikaInputStream tis = TikaInputStream.get(new byte[0])) {
+            parser.parse(tis, new DefaultHandler(), metadata, context);
+        }
+        return metadata;
+    }
+}
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java
 
b/tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java
index 5a5526b941..572d8ebe6f 100644
--- 
a/tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java
+++ 
b/tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java
@@ -530,10 +530,10 @@ public class TestParseContextSerialization {
         // SelfConfiguring, so they should be allowed to coexist.
         String json = """
                 {
-                  "configurable-test-parser": {
+                  "request-config-test-parser": {
                     "maxItems": 5
                   },
-                  "minimal-test-parser": {}
+                  "request-config-test-sub-parser": {}
                 }
                 """;
 
@@ -541,10 +541,10 @@ public class TestParseContextSerialization {
         // Should NOT throw - self-configuring components skip duplicate 
detection
         ParseContext deserialized = mapper.readValue(json, ParseContext.class);
 
-        assertTrue(deserialized.hasJsonConfig("configurable-test-parser"),
-                "configurable-test-parser should be stored as JSON config");
-        assertTrue(deserialized.hasJsonConfig("minimal-test-parser"),
-                "minimal-test-parser should be stored as JSON config");
+        assertTrue(deserialized.hasJsonConfig("request-config-test-parser"),
+                "request-config-test-parser should be stored as JSON config");
+        
assertTrue(deserialized.hasJsonConfig("request-config-test-sub-parser"),
+                "request-config-test-sub-parser should be stored as JSON 
config");
     }
 
     /**
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/serialization/WireRestrictedParseContextTest.java
 
b/tika-serialization/src/test/java/org/apache/tika/serialization/WireRestrictedParseContextTest.java
index 0ac2733371..ce092c655c 100644
--- 
a/tika-serialization/src/test/java/org/apache/tika/serialization/WireRestrictedParseContextTest.java
+++ 
b/tika-serialization/src/test/java/org/apache/tika/serialization/WireRestrictedParseContextTest.java
@@ -122,9 +122,9 @@ public class WireRestrictedParseContextTest {
         // Per-request tuning of an already-loaded self-configuring parser is 
config, not
         // instantiation: stored as an inert jsonConfig, never bound, so it 
must be allowed
         // (mirrors the real pdf-parser sortByPosition / OCR-strategy use 
case).
-        String json = "{\"configurable-test-parser\":{\"maxItems\":5}}";
+        String json = "{\"request-config-test-parser\":{\"maxItems\":5}}";
         ParseContext ctx = restrictedMapper().readValue(json, 
ParseContext.class);
-        assertTrue(ctx.hasJsonConfig("configurable-test-parser"));
+        assertTrue(ctx.hasJsonConfig("request-config-test-parser"));
     }
 
     @Test
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/serialization/serdes/UnreadBlockWarningTest.java
 
b/tika-serialization/src/test/java/org/apache/tika/serialization/serdes/UnreadBlockWarningTest.java
new file mode 100644
index 0000000000..e890d95f26
--- /dev/null
+++ 
b/tika-serialization/src/test/java/org/apache/tika/serialization/serdes/UnreadBlockWarningTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.serialization.serdes;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+public class UnreadBlockWarningTest {
+
+    @Test
+    public void testWarnsOncePerComponent() {
+        // a fixture no other test sends a block for, so the first call is 
this one
+        
assertTrue(ParseContextDeserializer.warnIfUnread("unread-warning-test-parser"));
+        
assertFalse(ParseContextDeserializer.warnIfUnread("unread-warning-test-parser"));
+        
assertFalse(ParseContextDeserializer.warnIfUnread("request-config-test-parser"));
+        
assertFalse(ParseContextDeserializer.warnIfUnread("no-such-component"));
+    }
+}
diff --git 
a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
 
b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
index 57365a8086..8b3ae9005f 100644
--- 
a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
+++ 
b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
@@ -290,6 +290,23 @@ public class TikaResourceTest extends CXFTestBase {
         assertContains("Happy New Year 2003!", responseMsg);
     }
 
+    @Test
+    public void testConfigForParserThatDoesNotReadItIsAccepted() throws 
Exception {
+        // mp4-parser takes extractSubtitles from tika-config.json only; 
warned about until 5.0
+        String configJson = "{\"mp4-parser\": {\"extractSubtitles\": false}}";
+        ContentDisposition fileCd = new ContentDisposition("form-data; 
name=\"file\"; filename=\"testOCR.pdf\"");
+        Attachment fileAtt = new Attachment("file",
+                
ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+        Attachment configAtt = new Attachment("config", "application/json",
+                new 
java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
+        Response response = WebClient
+                .create(endPoint + TIKA_PATH + "/config")
+                .type("multipart/form-data")
+                .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
+        assertEquals(200, response.getStatus());
+    }
+
     //TIKA-2290
     @Test
     public void testPDFOCRConfig() throws Exception {

Reply via email to