From 616a2d0baa2337df82ba3fb76863bd63d549d636 Mon Sep 17 00:00:00 2001 From: Mohit Joshi Date: Fri, 16 Oct 2026 13:37:42 +0530 Subject: [PATCH 1/5] fix(json): avoid duplicate root codec factory queries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../json/resolver/JsonSharedRegistry.java | 19 ++++++++-- .../fory/json/resolver/JsonTypeResolver.java | 37 +++++++++++++++++-- .../org/apache/fory/json/JsonModuleTest.java | 21 +++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index ce12aeacd7..42c4541306 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -899,6 +899,16 @@ ResolvedCodec resolveCodec( JsonTypeResolver localResolver, JsonCodecFactory childFactory, boolean runtimeType) { + return resolveCodec(rawType, typeRef, localResolver, childFactory, runtimeType, false); + } + + ResolvedCodec resolveCodec( + Class rawType, + TypeRef typeRef, + JsonTypeResolver localResolver, + JsonCodecFactory childFactory, + boolean runtimeType, + boolean skipModuleFactories) { JsonValueCodec customCodec = customCodec(rawType); if (customCodec != null) { return new ResolvedCodec(customCodec, null); @@ -944,7 +954,8 @@ ResolvedCodec resolveCodec( if (semanticToken) { // The exact JVM carrier codec cannot erase an explicit semantic type. The installed module // owns that representation even when the semantic value uses a primitive carrier. - ResolvedCodec resolved = createModuleCodec(typeRef, localResolver, runtimeType); + ResolvedCodec resolved = + skipModuleFactories ? null : createModuleCodec(typeRef, localResolver, runtimeType); if (resolved == null) { throw new ForyJsonException("No installed JSON module owns semantic type " + typeRef); } @@ -955,7 +966,8 @@ ResolvedCodec resolveCodec( return new ResolvedCodec(codec, null); } if (rawType == Object.class) { - ResolvedCodec resolved = createModuleCodec(typeRef, localResolver, runtimeType); + ResolvedCodec resolved = + skipModuleFactories ? null : createModuleCodec(typeRef, localResolver, runtimeType); if (resolved != null) { return resolved; } @@ -1009,7 +1021,8 @@ ResolvedCodec resolveCodec( if (Path.class.isAssignableFrom(rawType)) { return new ResolvedCodec(ScalarCodecs.PathCodec.INSTANCE, null); } - ResolvedCodec resolved = createModuleCodec(typeRef, localResolver, runtimeType); + ResolvedCodec resolved = + skipModuleFactories ? null : createModuleCodec(typeRef, localResolver, runtimeType); if (resolved != null) { return resolved; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 089e4154a0..9aa8925ba5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -541,6 +541,11 @@ private JsonTypeInfo resolveTypeInfo(Type declaredType, Class rawType, Object } private JsonTypeInfo resolveTypeInfo(TypeRef declaredType, Object key) { + return resolveTypeInfo(declaredType, key, false); + } + + private JsonTypeInfo resolveTypeInfo( + TypeRef declaredType, Object key, boolean skipModuleFactories) { validateCovariant(declaredType); Class rawType = declaredType.getRawType(); JsonTypeInfo typeInfo = customTypeInfo(declaredType, rawType); @@ -560,7 +565,7 @@ private JsonTypeInfo resolveTypeInfo(TypeRef declaredType, Object key) { codec.resolveTypes(declaredType, this); return typeInfo; } - return buildTypeInfo(rawType, declaredType, key); + return buildTypeInfo(rawType, declaredType, key, null, skipModuleFactories); } private JsonTypeInfo resolveTypeInfo(TypeRef declaredType, JsonCodec annotation) { @@ -1024,7 +1029,21 @@ private JsonTypeInfo resolveRuntimeTypeInfo(Class runtimeType) { // The default Java object model is the declared schema for this exact class. Reuse that // binding so generated capabilities stay on the direct root path. Only a codec actually // selected by a runtime factory needs the runtime-only publication below. - typeInfo = getTypeInfo(typeRef); + typeInfo = typeInfos.get(runtimeType); + if (typeInfo == null) { + // The runtime factory already declined this root, so the declared fallback must not ask + // the same factory again as if the root were a field occurrence. + ResolutionSnapshot snapshot = beginResolution(); + try { + typeInfo = resolveTypeInfo(typeRef, resolutionTypeKey(typeRef), true); + completeResolution(snapshot); + } catch (RuntimeException | Error e) { + rollbackResolution(snapshot); + throw e; + } finally { + endResolution(); + } + } runtimeTypeInfos.put(runtimeType, typeInfo); return typeInfo; } @@ -2711,14 +2730,24 @@ private ObjectCodec newObjectCodec(TypeRef ownerType) { } private JsonTypeInfo buildTypeInfo(Class rawType, TypeRef typeRef, Object key) { - return buildTypeInfo(rawType, typeRef, key, null); + return buildTypeInfo(rawType, typeRef, key, null, false); } private JsonTypeInfo buildTypeInfo( Class rawType, TypeRef typeRef, Object key, JsonCodecFactory childFactory) { + return buildTypeInfo(rawType, typeRef, key, childFactory, false); + } + + private JsonTypeInfo buildTypeInfo( + Class rawType, + TypeRef typeRef, + Object key, + JsonCodecFactory childFactory, + boolean skipModuleFactories) { sharedRegistry.checkSecure(rawType); JsonSharedRegistry.ResolvedCodec resolved = - sharedRegistry.resolveCodec(rawType, typeRef, this, childFactory, false); + sharedRegistry.resolveCodec( + rawType, typeRef, this, childFactory, false, skipModuleFactories); if (resolved == null) { return buildObjectTypeInfo(typeRef, key); } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java index 0842c39ca2..5e92f359b2 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java @@ -59,6 +59,27 @@ import org.testng.annotations.Test; public class JsonModuleTest { + public static class FactoryPet {} + + @Test + public void factoryDoesNotRequeryDeclinedRoot() { + List runtimeTypes = new ArrayList<>(); + JsonCodecFactory factory = + (type, resolver, runtimeType) -> { + if (type.getRawType() == FactoryPet.class) { + runtimeTypes.add(runtimeType); + } + return null; + }; + ForyJson json = + ForyJson.builder() + .withModule(context -> context.registerCodecFactory(factory)) + .build(); + + json.toJson(new FactoryPet()); + assertEquals(runtimeTypes, Arrays.asList(true)); + } + @Test public void installsOnceAndFreezesContext() { AtomicInteger installs = new AtomicInteger(); From 9cd292fd43e57f4a00c0b239789e3d15524220e2 Mon Sep 17 00:00:00 2001 From: Mohit Joshi Date: Fri, 16 Oct 2026 15:38:36 +0530 Subject: [PATCH 2/5] fix(json): preserve declared factory lookup after root fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/org/apache/fory/json/resolver/JsonTypeResolver.java | 4 ++++ .../src/test/java/org/apache/fory/json/JsonModuleTest.java | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 9aa8925ba5..0489f6d3b3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -1036,6 +1036,10 @@ private JsonTypeInfo resolveRuntimeTypeInfo(Class runtimeType) { ResolutionSnapshot snapshot = beginResolution(); try { typeInfo = resolveTypeInfo(typeRef, resolutionTypeKey(typeRef), true); + Object key = resolutionTypeKey(typeRef); + if (typeInfos.get(key) == typeInfo) { + typeInfos.remove(key); + } completeResolution(snapshot); } catch (RuntimeException | Error e) { rollbackResolution(snapshot); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java index 5e92f359b2..0ff955a436 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java @@ -77,7 +77,8 @@ public void factoryDoesNotRequeryDeclinedRoot() { .build(); json.toJson(new FactoryPet()); - assertEquals(runtimeTypes, Arrays.asList(true)); + json.fromJson("{}", FactoryPet.class); + assertEquals(runtimeTypes, Arrays.asList(true, false)); } @Test From 22a6539c6c7ac45d9ee2ea1a9eebf34a50fe97bc Mon Sep 17 00:00:00 2001 From: Mohit Joshi Date: Fri, 16 Oct 2026 19:08:07 +0530 Subject: [PATCH 3/5] fix(json): preserve default metadata without factories Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../org/apache/fory/json/resolver/JsonSharedRegistry.java | 4 ++++ .../java/org/apache/fory/json/resolver/JsonTypeResolver.java | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index 42c4541306..7ad4ddc757 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -1047,6 +1047,10 @@ JsonValueCodec createRuntimeCodec(Class rawType, JsonTypeResolver localRes return resolved == null ? null : resolved.codec; } + boolean hasCodecFactories() { + return codecFactories.length != 0; + } + private JsonValueCodec createExactCodec( Class target, TypeRef typeRef, diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 0489f6d3b3..aef884a385 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -1029,8 +1029,9 @@ private JsonTypeInfo resolveRuntimeTypeInfo(Class runtimeType) { // The default Java object model is the declared schema for this exact class. Reuse that // binding so generated capabilities stay on the direct root path. Only a codec actually // selected by a runtime factory needs the runtime-only publication below. - typeInfo = typeInfos.get(runtimeType); - if (typeInfo == null) { + if (!sharedRegistry.hasCodecFactories()) { + typeInfo = getTypeInfo(runtimeType, runtimeType); + } else { // The runtime factory already declined this root, so the declared fallback must not ask // the same factory again as if the root were a field occurrence. ResolutionSnapshot snapshot = beginResolution(); From c04a6aae0e539185f50bb3de32cb7b6b84b3cc98 Mon Sep 17 00:00:00 2001 From: Mohit Joshi Date: Fri, 16 Oct 2026 19:20:58 +0530 Subject: [PATCH 4/5] style(json): format codec factory regression test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/test/java/org/apache/fory/json/JsonModuleTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java index 0ff955a436..53f30d1fc7 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java @@ -71,10 +71,7 @@ public void factoryDoesNotRequeryDeclinedRoot() { } return null; }; - ForyJson json = - ForyJson.builder() - .withModule(context -> context.registerCodecFactory(factory)) - .build(); + ForyJson json = ForyJson.builder().withModule(context -> context.registerCodecFactory(factory)).build(); json.toJson(new FactoryPet()); json.fromJson("{}", FactoryPet.class); From cd48f35b3a8c571cce632e8bd799b658835b5730 Mon Sep 17 00:00:00 2001 From: Mohit Joshi Date: Fri, 16 Oct 2026 19:37:43 +0530 Subject: [PATCH 5/5] style(json): follow spotless builder formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/test/java/org/apache/fory/json/JsonModuleTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java index 53f30d1fc7..ff768d46ea 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java @@ -71,7 +71,8 @@ public void factoryDoesNotRequeryDeclinedRoot() { } return null; }; - ForyJson json = ForyJson.builder().withModule(context -> context.registerCodecFactory(factory)).build(); + ForyJson json = + ForyJson.builder().withModule(context -> context.registerCodecFactory(factory)).build(); json.toJson(new FactoryPet()); json.fromJson("{}", FactoryPet.class);