This is an automated email from the ASF dual-hosted git repository.
tallison 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 64b411d1b1 TIKA-4613 -- look for jsonconfig constructor, fall back to
no-arg (#2516)
64b411d1b1 is described below
commit 64b411d1b1316d3be2fa84c38a44fad32b74ffef
Author: Tim Allison <[email protected]>
AuthorDate: Wed Jan 7 10:58:22 2026 -0500
TIKA-4613 -- look for jsonconfig constructor, fall back to no-arg (#2516)
* TIKA-4613 -- look for jsonconfig constructor, fall back to no-arg, and
further refinements
---
.../tika/annotation/TikaComponentProcessor.java | 13 +++
.../tika/sax/BasicContentHandlerFactory.java | 2 +-
.../config/ComponentRegistryIntegrationTest.java | 12 +-
.../tika/serialization/ParseContextUtils.java | 129 ++++++++-------------
.../org/apache/tika/serialization/TikaModule.java | 13 +--
.../tika/metadata/filter/JsonConfigOnlyFilter.java | 66 +++++++++++
.../tika/metadata/filter/TestMetadataFilter.java | 19 +++
.../TestParseContextSerialization.java | 87 ++++++++++++++
.../configs/TIKA-4582-json-config-only.json | 9 ++
9 files changed, 257 insertions(+), 93 deletions(-)
diff --git
a/tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java
b/tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java
index 9e818627c6..cd757664e8 100644
---
a/tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java
+++
b/tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java
@@ -142,9 +142,22 @@ public class TikaComponentProcessor extends
AbstractProcessor {
List<String> serviceInterfaces = findServiceInterfaces(element);
// Build the index entry value (className or className:key=X)
+ // Auto-detect contextKey from service interface if not explicitly
specified
String indexValue = className;
if (contextKey != null) {
+ // Explicit contextKey specified
indexValue = className + ":key=" + contextKey;
+ } else if (serviceInterfaces.size() == 1) {
+ // Auto-detect contextKey from single service interface
+ indexValue = className + ":key=" + serviceInterfaces.get(0);
+ messager.printMessage(Diagnostic.Kind.NOTE,
+ "Auto-detected contextKey=" + serviceInterfaces.get(0) + "
for " + className);
+ } else if (serviceInterfaces.size() > 1) {
+ // Multiple interfaces - warn that contextKey should be specified
+ messager.printMessage(Diagnostic.Kind.WARNING,
+ "Class " + className + " implements multiple service
interfaces: " +
+ serviceInterfaces + ". Consider specifying
@TikaComponent(contextKey=...) " +
+ "to select which one to use as ParseContext key.",
element);
}
if (serviceInterfaces.isEmpty()) {
diff --git
a/tika-core/src/main/java/org/apache/tika/sax/BasicContentHandlerFactory.java
b/tika-core/src/main/java/org/apache/tika/sax/BasicContentHandlerFactory.java
index 2612ec8650..87bc0bad82 100644
---
a/tika-core/src/main/java/org/apache/tika/sax/BasicContentHandlerFactory.java
+++
b/tika-core/src/main/java/org/apache/tika/sax/BasicContentHandlerFactory.java
@@ -35,7 +35,7 @@ import org.apache.tika.parser.ParseContext;
* Implements {@link StreamingContentHandlerFactory} to support both in-memory
* content extraction and streaming output to an OutputStream.
*/
-@TikaComponent(contextKey = ContentHandlerFactory.class)
+@TikaComponent
public class BasicContentHandlerFactory implements
StreamingContentHandlerFactory, WriteLimiter {
private HANDLER_TYPE type = HANDLER_TYPE.TEXT;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/config/ComponentRegistryIntegrationTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/config/ComponentRegistryIntegrationTest.java
index 09c3460979..3c12e3df73 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/config/ComponentRegistryIntegrationTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/config/ComponentRegistryIntegrationTest.java
@@ -229,7 +229,8 @@ public class ComponentRegistryIntegrationTest {
}
/**
- * Reads an index file in the format: name=fully.qualified.ClassName
+ * Reads an index file in the format:
name=fully.qualified.ClassName[:key=contextKeyClass]
+ * Returns a map of component name -> class name (without the :key=
suffix).
*/
private Map<String, String> readIndexFile(InputStream stream) throws
Exception {
Map<String, String> index = new HashMap<>();
@@ -243,7 +244,14 @@ public class ComponentRegistryIntegrationTest {
}
String[] parts = line.split("=", 2);
if (parts.length == 2) {
- index.put(parts[0].trim(), parts[1].trim());
+ String name = parts[0].trim();
+ String value = parts[1].trim();
+ // Strip optional :key=contextKeyClass suffix
+ int colonIndex = value.indexOf(':');
+ if (colonIndex > 0) {
+ value = value.substring(0, colonIndex);
+ }
+ index.put(name, value);
}
}
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
index e530c1951b..10fe4ac927 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
@@ -29,7 +29,6 @@ import org.slf4j.LoggerFactory;
import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.loader.ComponentInfo;
import org.apache.tika.config.loader.ComponentInstantiator;
-import org.apache.tika.config.loader.ComponentRegistry;
import org.apache.tika.config.loader.TikaObjectMapperFactory;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.metadata.filter.CompositeMetadataFilter;
@@ -59,18 +58,6 @@ public class ParseContextUtils {
private static final Logger LOG =
LoggerFactory.getLogger(ParseContextUtils.class);
private static final ObjectMapper MAPPER =
TikaObjectMapperFactory.getMapper();
- /**
- * Known interfaces that should be used as ParseContext keys.
- * When a component implements one of these interfaces, the interface is
used as
- * the key in ParseContext instead of the concrete class.
- * <p>
- * These are NOT auto-discovered via SPI - they require explicit
configuration.
- */
- private static final List<Class<?>> KNOWN_CONTEXT_INTERFACES = List.of(
- MetadataFilter.class
- // Add other known interfaces as needed
- );
-
/**
* Mapping of array config keys to their context keys and composite
wrapper factories.
* Key: config name (e.g., "metadata-filters")
@@ -88,18 +75,17 @@ public class ParseContextUtils {
/**
* Resolves all JSON configs from ParseContext and adds them to the
resolved cache.
* <p>
- * Iterates through all entries in jsonConfigs, looks up the friendly name
in ComponentRegistry,
+ * Iterates through all entries in jsonConfigs, looks up the friendly name
in
+ * ComponentNameResolver (which searches all registered component
registries),
* deserializes the JSON, and caches the instance in resolvedConfigs.
* <p>
* Components that implement {@link
org.apache.tika.config.SelfConfiguring} are skipped -
* they read their own config at runtime via {@link ConfigDeserializer}.
* <p>
- * The ParseContext key is determined by:
- * <ol>
- * <li>Explicit contextKey from @TikaComponent annotation (if
specified)</li>
- * <li>Auto-detected from {@link #KNOWN_CONTEXT_INTERFACES} (if
component implements one)</li>
- * <li>The component's own class (default)</li>
- * </ol>
+ * The ParseContext key is determined by the contextKey from the .idx
file, which is
+ * auto-detected by the annotation processor from the service interface,
or explicitly
+ * specified via {@code @TikaComponent(contextKey=...)}. Falls back to the
component
+ * class if no contextKey is available.
*
* @param context the ParseContext to populate
* @param classLoader the ClassLoader to use for loading component classes
@@ -125,87 +111,68 @@ public class ParseContextUtils {
}
}
- // Then, try to load the "other-configs" registry for single component
configs
- try {
- ComponentRegistry registry = new
ComponentRegistry("other-configs", classLoader);
+ // Then, try to resolve single component configs using
ComponentNameResolver
+ // This searches all registered component registries, not just
"other-configs"
+ for (Map.Entry<String, JsonConfig> entry : jsonConfigs.entrySet()) {
+ String friendlyName = entry.getKey();
+ JsonConfig jsonConfig = entry.getValue();
- for (Map.Entry<String, JsonConfig> entry : jsonConfigs.entrySet())
{
- String friendlyName = entry.getKey();
- JsonConfig jsonConfig = entry.getValue();
+ // Skip already resolved configs (including array configs)
+ if (context.getResolvedConfig(friendlyName) != null) {
+ continue;
+ }
- // Skip already resolved configs (including array configs)
- if (context.getResolvedConfig(friendlyName) != null) {
- continue;
- }
+ // Try to find this friendly name in any registered component
registry
+ var optionalInfo =
ComponentNameResolver.getComponentInfo(friendlyName);
+ if (optionalInfo.isEmpty()) {
+ // Not a registered component - that's okay, might be used for
something else
+ LOG.debug("'{}' not found in any component registry,
skipping", friendlyName);
+ continue;
+ }
- ComponentInfo info = null;
- try {
- // Try to find this friendly name in the registry
- info = registry.getComponentInfo(friendlyName);
+ ComponentInfo info = optionalInfo.get();
- // Skip self-configuring components - they handle their
own config
- if (info.selfConfiguring()) {
- LOG.debug("'{}' is self-configuring, skipping
resolution", friendlyName);
- continue;
- }
+ // Skip self-configuring components - they handle their own config
+ if (info.selfConfiguring()) {
+ LOG.debug("'{}' is self-configuring, skipping resolution",
friendlyName);
+ continue;
+ }
- // Determine the context key
- Class<?> contextKey = determineContextKey(info,
friendlyName);
+ // Determine the context key
+ Class<?> contextKey = determineContextKey(info);
- // Deserialize and cache in resolvedConfigs, also add to
context
- Object instance = MAPPER.readValue(jsonConfig.json(),
info.componentClass());
- context.setResolvedConfig(friendlyName, instance);
- context.set((Class) contextKey, instance);
+ try {
+ // Deserialize and cache in resolvedConfigs, also add to
context
+ Object instance = MAPPER.readValue(jsonConfig.json(),
info.componentClass());
+ context.setResolvedConfig(friendlyName, instance);
+ context.set((Class) contextKey, instance);
- LOG.debug("Resolved '{}' -> {} with key {}",
- friendlyName, info.componentClass().getName(),
contextKey.getName());
- } catch (TikaConfigException e) {
- // Not a registered component - that's okay, might be used
for something else
- LOG.debug("'{}' not found in other-configs registry,
skipping", friendlyName);
- } catch (IOException e) {
- LOG.warn("Failed to deserialize component '{}' of type
{}", friendlyName,
- info != null ? info.componentClass().getName() :
"unknown", e);
- }
+ LOG.debug("Resolved '{}' -> {} with key {}",
+ friendlyName, info.componentClass().getName(),
contextKey.getName());
+ } catch (IOException e) {
+ LOG.warn("Failed to deserialize component '{}' of type {}",
friendlyName,
+ info.componentClass().getName(), e);
}
- } catch (TikaConfigException e) {
- // other-configs registry not available - that's okay, array
configs were still processed
- LOG.debug("other-configs registry not available: {}",
e.getMessage());
}
}
/**
* Determines the ParseContext key for a component.
+ * <p>
+ * The contextKey is auto-detected by the annotation processor from the
service
+ * interface implemented by the component. If not detected (e.g.,
component implements
+ * multiple interfaces), falls back to the component class.
*
* @param info the component info
- * @param friendlyName the component's friendly name (for error messages)
* @return the class to use as ParseContext key
- * @throws TikaConfigException if the component implements multiple known
interfaces
- * and no explicit contextKey is specified
*/
- private static Class<?> determineContextKey(ComponentInfo info, String
friendlyName)
- throws TikaConfigException {
- // Use explicit contextKey if provided
+ private static Class<?> determineContextKey(ComponentInfo info) {
+ // Use contextKey from .idx file (auto-detected or explicit from
@TikaComponent)
if (info.contextKey() != null) {
return info.contextKey();
}
-
- // Auto-detect from known interfaces
- List<Class<?>> matches = new ArrayList<>();
- for (Class<?> iface : KNOWN_CONTEXT_INTERFACES) {
- if (iface.isAssignableFrom(info.componentClass())) {
- matches.add(iface);
- }
- }
-
- if (matches.size() > 1) {
- throw new TikaConfigException(
- "Component '" + friendlyName + "' (" +
info.componentClass().getName() +
- ") implements multiple known context interfaces: " +
matches +
- ". Use @TikaComponent(contextKey=...) to specify which one
to use.");
- }
-
- // Use the single matched interface, or fall back to the component
class
- return matches.isEmpty() ? info.componentClass() : matches.get(0);
+ // Fall back to the component class itself
+ return info.componentClass();
}
/**
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
index 249f7f71cf..181e6b90e1 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
@@ -227,12 +227,9 @@ public class TikaModule extends SimpleModule {
*/
private static class TikaComponentDeserializer extends
JsonDeserializer<Object> {
private final Class<?> expectedType;
- // Plain mapper for property updates (avoids infinite recursion with
registered types)
- private final ObjectMapper plainMapper;
TikaComponentDeserializer(Class<?> expectedType) {
this.expectedType = expectedType;
- this.plainMapper = new ObjectMapper();
}
@Override
@@ -307,19 +304,17 @@ public class TikaModule extends SimpleModule {
} else if (cleanedConfig == null || cleanedConfig.isEmpty()) {
// If no config, use default constructor
instance = clazz.getDeclaredConstructor().newInstance();
- } else if (SelfConfiguring.class.isAssignableFrom(clazz)) {
- // SelfConfiguring components: prefer JsonConfig
constructor if available
+ } else {
+ // Try JsonConfig constructor first (works for any
component)
Constructor<?> jsonConfigCtor =
findJsonConfigConstructor(clazz);
if (jsonConfigCtor != null) {
String json = mapper.writeValueAsString(cleanedConfig);
instance = jsonConfigCtor.newInstance((JsonConfig) ()
-> json);
} else {
+ // Fall back to no-arg constructor + Jackson bean
deserialization
instance =
clazz.getDeclaredConstructor().newInstance();
+
mapper.readerForUpdating(instance).readValue(cleanedConfig);
}
- } else {
- // Non-SelfConfiguring: use Jackson bean deserialization
- instance = clazz.getDeclaredConstructor().newInstance();
-
plainMapper.readerForUpdating(instance).readValue(cleanedConfig);
}
// Call initialize() on Initializable components
diff --git
a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/JsonConfigOnlyFilter.java
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/JsonConfigOnlyFilter.java
new file mode 100644
index 0000000000..aad030381c
--- /dev/null
+++
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/JsonConfigOnlyFilter.java
@@ -0,0 +1,66 @@
+/*
+ * 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.metadata.filter;
+
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.TikaComponent;
+import org.apache.tika.metadata.Metadata;
+
+/**
+ * Test filter that ONLY has a JsonConfig constructor (no no-arg constructor).
+ * Used to verify that TikaModule correctly handles components without no-arg
constructors.
+ */
+@TikaComponent
+public class JsonConfigOnlyFilter extends MetadataFilterBase {
+
+ public static class Config {
+ public String prefix = "";
+ }
+
+ private final String prefix;
+
+ /**
+ * Constructor that requires JsonConfig - no no-arg constructor available.
+ */
+ public JsonConfigOnlyFilter(JsonConfig jsonConfig) {
+ Config config = ConfigDeserializer.buildConfig(jsonConfig,
Config.class);
+ this.prefix = config.prefix;
+ }
+
+ /**
+ * Constructor with explicit Config object for programmatic use.
+ */
+ public JsonConfigOnlyFilter(Config config) {
+ this.prefix = config.prefix;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @Override
+ protected void filter(Metadata metadata) {
+ for (String name : metadata.names()) {
+ String[] values = metadata.getValues(name);
+ metadata.remove(name);
+ for (String value : values) {
+ metadata.add(name, prefix + value);
+ }
+ }
+ }
+}
diff --git
a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/TestMetadataFilter.java
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/TestMetadataFilter.java
index 197a116702..04363eeec3 100644
---
a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/TestMetadataFilter.java
+++
b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/TestMetadataFilter.java
@@ -283,6 +283,25 @@ public class TestMetadataFilter extends TikaTest {
assertEquals(2, metadata.names().length);
}
+ /**
+ * Test that TikaModule correctly instantiates components that only have a
JsonConfig
+ * constructor (no no-arg constructor). This verifies the fix for
TIKA-4582.
+ */
+ @Test
+ public void testJsonConfigOnlyFilter() throws Exception {
+ TikaLoader loader = TikaLoader.load(getConfigPath(getClass(),
"TIKA-4582-json-config-only.json"));
+ Metadata metadata = new Metadata();
+ metadata.set("title", "my title");
+ metadata.set("author", "my author");
+
+ MetadataFilter filter = loader.get(MetadataFilter.class);
+ metadata = filterOne(filter, metadata);
+
+ assertEquals(2, metadata.size());
+ assertEquals("TEST_my title", metadata.get("title"));
+ assertEquals("TEST_my author", metadata.get("author"));
+ }
+
private static Metadata filterOne(MetadataFilter filter, Metadata
singleMetadata) throws TikaException {
List<Metadata> list = new ArrayList<>();
list.add(singleMetadata);
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 c8fd0e4221..1df1567073 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
@@ -41,6 +41,8 @@ import org.apache.tika.metadata.filter.MockUpperCaseFilter;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.PasswordProvider;
import org.apache.tika.parser.SimplePasswordProvider;
+import org.apache.tika.sax.BasicContentHandlerFactory;
+import org.apache.tika.sax.ContentHandlerFactory;
import org.apache.tika.serialization.serdes.ParseContextDeserializer;
import org.apache.tika.serialization.serdes.ParseContextSerializer;
@@ -342,4 +344,89 @@ public class TestParseContextSerialization {
assertEquals("secret123", provider.getPassword(null),
"Password should match the configured value");
}
+
+ /**
+ * Test that BasicContentHandlerFactory can be configured via JSON,
serialized,
+ * deserialized, and resolved via ParseContextUtils.resolveAll().
+ * This verifies the fix for TIKA-4582 where ContentHandlerFactory was not
being
+ * resolved because it wasn't in the "other-configs" registry.
+ */
+ @Test
+ public void testContentHandlerFactoryRoundTrip() throws Exception {
+ // Create ParseContext with BasicContentHandlerFactory configuration
+ String json = """
+ {
+ "basic-content-handler-factory": {
+ "type": "XML",
+ "writeLimit": 50000
+ }
+ }
+ """;
+
+ ObjectMapper mapper = createMapper();
+ ParseContext deserialized = mapper.readValue(json, ParseContext.class);
+
+ // Verify JSON config is present
+ assertTrue(deserialized.hasJsonConfig("basic-content-handler-factory"),
+ "Should have basic-content-handler-factory JSON config");
+
+ // Resolve the config - this should now work with ComponentNameResolver
+ ParseContextUtils.resolveAll(deserialized,
Thread.currentThread().getContextClassLoader());
+
+ // Should be accessible via ContentHandlerFactory.class (the
contextKey)
+ ContentHandlerFactory factory =
deserialized.get(ContentHandlerFactory.class);
+ assertNotNull(factory, "ContentHandlerFactory should be resolved");
+ assertTrue(factory instanceof BasicContentHandlerFactory,
+ "Should be BasicContentHandlerFactory instance");
+
+ // Verify the configuration was applied
+ BasicContentHandlerFactory basicFactory = (BasicContentHandlerFactory)
factory;
+ assertEquals(BasicContentHandlerFactory.HANDLER_TYPE.XML,
basicFactory.getType(),
+ "Handler type should be XML");
+ assertEquals(50000, basicFactory.getWriteLimit(),
+ "Write limit should be 50000");
+ }
+
+ /**
+ * Test full round-trip: create ParseContext with ContentHandlerFactory,
+ * serialize to JSON, deserialize back, resolve, and verify.
+ */
+ @Test
+ public void testContentHandlerFactoryFullRoundTrip() throws Exception {
+ // Create original ParseContext with JSON config
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("basic-content-handler-factory", """
+ {
+ "type": "HTML",
+ "writeLimit": 10000,
+ "throwOnWriteLimitReached": false
+ }
+ """);
+
+ // Serialize
+ ObjectMapper mapper = createMapper();
+ String json = mapper.writeValueAsString(original);
+
+ // Verify JSON structure
+ JsonNode root = mapper.readTree(json);
+ assertTrue(root.has("basic-content-handler-factory"),
+ "Serialized JSON should have basic-content-handler-factory");
+
+ // Deserialize
+ ParseContext deserialized = mapper.readValue(json, ParseContext.class);
+ assertTrue(deserialized.hasJsonConfig("basic-content-handler-factory"),
+ "Deserialized should have JSON config");
+
+ // Resolve
+ ParseContextUtils.resolveAll(deserialized,
Thread.currentThread().getContextClassLoader());
+
+ // Verify resolution
+ ContentHandlerFactory factory =
deserialized.get(ContentHandlerFactory.class);
+ assertNotNull(factory, "ContentHandlerFactory should be resolved after
round-trip");
+
+ BasicContentHandlerFactory basicFactory = (BasicContentHandlerFactory)
factory;
+ assertEquals(BasicContentHandlerFactory.HANDLER_TYPE.HTML,
basicFactory.getType());
+ assertEquals(10000, basicFactory.getWriteLimit());
+ assertFalse(basicFactory.isThrowOnWriteLimitReached());
+ }
}
diff --git
a/tika-serialization/src/test/resources/configs/TIKA-4582-json-config-only.json
b/tika-serialization/src/test/resources/configs/TIKA-4582-json-config-only.json
new file mode 100644
index 0000000000..bf57d99c31
--- /dev/null
+++
b/tika-serialization/src/test/resources/configs/TIKA-4582-json-config-only.json
@@ -0,0 +1,9 @@
+{
+ "metadata-filters": [
+ {
+ "json-config-only-filter": {
+ "prefix": "TEST_"
+ }
+ }
+ ]
+}