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_"
+      }
+    }
+  ]
+}

Reply via email to