From 2c9a99412d3ea2e84ee3d350fc0facf3fe92492e Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Mon, 27 Jul 2026 01:18:31 +0800 Subject: [PATCH 1/8] fix: use Unsafe.allocateInstance for non-static inner class to avoid JDK 25 NPE (#7654) JDK 25 adds Objects.requireNonNull for the enclosing instance parameter in inner class constructors. The previous approach of passing null and patching this$0 afterwards now throws NPE before the patch can occur. Use Unsafe.allocateInstance to bypass the constructor entirely for non-static inner classes; the existing this$0 fixup via Unsafe.putObject still sets the enclosing instance correctly after creation. --- .../fastjson2/reader/ConstructorSupplier.java | 3 +- .../fastjson2/reader/ObjectReaderAdapter.java | 8 +- .../reader/ObjectReaderCreatorASM.java | 6 +- .../fastjson2/issues_7000/Issue7654.java | 81 +++++++++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java index bade6d1ecf..58fab0091c 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java @@ -1,6 +1,7 @@ package com.alibaba.fastjson2.reader; import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.util.JDKUtils; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; @@ -28,7 +29,7 @@ public Object get() { return objectClass.newInstance(); } else { if (constructor.getParameterCount() == 1) { - return constructor.newInstance(new Object[1]); + return JDKUtils.UNSAFE.allocateInstance(objectClass); } else { return constructor.newInstance(); } diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java index 13a792c295..dc2c8196e1 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson2.schema.JSONSchema; import com.alibaba.fastjson2.util.BeanUtils; import com.alibaba.fastjson2.util.Fnv; +import com.alibaba.fastjson2.util.JDKUtils; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -443,7 +444,12 @@ public T createInstance(long features) { if (constructor != null) { try { - T object = (T) constructor.newInstance(new Object[parameterCount]); + T object; + if (parameterCount == 0) { + object = (T) constructor.newInstance(); + } else { + object = (T) JDKUtils.UNSAFE.allocateInstance(objectClass); + } if (hasDefaultValue) { initDefaultValue(object); } diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java index 8ea085ee78..fba8520808 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java @@ -620,7 +620,8 @@ private ObjectReaderBean jitObjectReader( String methodName = fieldBased && defaultConstructor == null ? "createInstance0" : "createInstance"; if ((externalClass && defaultConstructor != null) - || fieldBased && (defaultConstructor == null || !Modifier.isPublic(defaultConstructor.getModifiers()) || !Modifier.isPublic(objectClass.getModifiers()))) { + || fieldBased && (defaultConstructor == null || !Modifier.isPublic(defaultConstructor.getModifiers()) || !Modifier.isPublic(objectClass.getModifiers())) + || (defaultConstructor != null && defaultConstructor.getParameterCount() != 0)) { MethodWriter mw = cw.visitMethod( Opcodes.ACC_PUBLIC, methodName, @@ -2953,7 +2954,8 @@ private void genCreateObject( int objectModifiers = objectClass == null ? Modifier.PUBLIC : objectClass.getModifiers(); boolean publicObject = Modifier.isPublic(objectModifiers) && (objectClass == null || !classLoader.isExternalClass(objectClass)); - if (defaultConstructor == null || !publicObject || !Modifier.isPublic(defaultConstructor.getModifiers())) { + if (defaultConstructor == null || !publicObject || !Modifier.isPublic(defaultConstructor.getModifiers()) + || defaultConstructor.getParameterCount() != 0) { if (creator != null) { mw.aload(THIS); mw.getfield(classNameType, "creator", "Ljava/util/function/Supplier;"); diff --git a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java new file mode 100644 index 0000000000..d992c29a2a --- /dev/null +++ b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java @@ -0,0 +1,81 @@ +package com.alibaba.fastjson2.issues_7000; + +import com.alibaba.fastjson2.JSON; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class Issue7654 { + @Test + public void testNonStaticInnerClass() { + String json = "{\"issues\":[],\"summary\":{\"errors\":0,\"warnings\":0,\"messagesProcessed\":7,\"messagesAccepted\":7,\"messagesInvalid\":0}}"; + AmzListingJsonFeedResult result = JSON.parseObject(json, AmzListingJsonFeedResult.class); + assertNotNull(result.getSummary()); + assertEquals(7, result.getSummary().getMessagesAccepted()); + assertEquals(7, result.getSummary().getMessagesProcessed()); + assertEquals(0, result.getSummary().getErrors()); + assertEquals(0, result.getSummary().getWarnings()); + assertEquals(0, result.getSummary().getMessagesInvalid()); + } + + public static class AmzListingJsonFeedResult { + private Summary summary; + + public Summary getSummary() { + return summary; + } + + public void setSummary(Summary summary) { + this.summary = summary; + } + + public class Summary { + private Integer errors; + private Integer warnings; + private Integer messagesProcessed; + private Integer messagesAccepted; + private Integer messagesInvalid; + + public Integer getErrors() { + return errors; + } + + public void setErrors(Integer errors) { + this.errors = errors; + } + + public Integer getWarnings() { + return warnings; + } + + public void setWarnings(Integer warnings) { + this.warnings = warnings; + } + + public Integer getMessagesProcessed() { + return messagesProcessed; + } + + public void setMessagesProcessed(Integer messagesProcessed) { + this.messagesProcessed = messagesProcessed; + } + + public Integer getMessagesAccepted() { + return messagesAccepted; + } + + public void setMessagesAccepted(Integer messagesAccepted) { + this.messagesAccepted = messagesAccepted; + } + + public Integer getMessagesInvalid() { + return messagesInvalid; + } + + public void setMessagesInvalid(Integer messagesInvalid) { + this.messagesInvalid = messagesInvalid; + } + } + } +} From 9a54a6df73061c0915f0455b654aafab7d7e8e65 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Mon, 27 Jul 2026 01:39:42 +0800 Subject: [PATCH 2/8] fix: use dummy enclosing instance instead of Unsafe.allocateInstance to preserve constructor semantics Unsafe.allocateInstance bypassed field initializers (Issue367) and constructor logic (Issue1082). Instead, create a dummy enclosing instance via Unsafe.allocateInstance(declaringClass) and pass it to the inner class constructor, preserving field defaults and validation. The real parent is still patched via Unsafe.putObject afterwards. --- .../fastjson2/reader/ConstructorSupplier.java | 4 +++- .../fastjson2/reader/ObjectReaderAdapter.java | 4 +++- .../fastjson2/reader/ObjectReaderCreatorASM.java | 11 ++++++----- .../fastjson2/v1issues/issue_1000/Issue1082.java | 12 ++---------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java index 58fab0091c..fa001f7c8e 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java @@ -29,7 +29,9 @@ public Object get() { return objectClass.newInstance(); } else { if (constructor.getParameterCount() == 1) { - return JDKUtils.UNSAFE.allocateInstance(objectClass); + Class paramType = constructor.getParameterTypes()[0]; + Object dummy = JDKUtils.UNSAFE.allocateInstance(paramType); + return constructor.newInstance(dummy); } else { return constructor.newInstance(); } diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java index dc2c8196e1..06478ebc37 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java @@ -448,7 +448,9 @@ public T createInstance(long features) { if (parameterCount == 0) { object = (T) constructor.newInstance(); } else { - object = (T) JDKUtils.UNSAFE.allocateInstance(objectClass); + Class paramType = constructor.getParameterTypes()[0]; + Object dummy = JDKUtils.UNSAFE.allocateInstance(paramType); + object = (T) constructor.newInstance(dummy); } if (hasDefaultValue) { initDefaultValue(object); diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java index fba8520808..f5b4a3c933 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java @@ -620,8 +620,7 @@ private ObjectReaderBean jitObjectReader( String methodName = fieldBased && defaultConstructor == null ? "createInstance0" : "createInstance"; if ((externalClass && defaultConstructor != null) - || fieldBased && (defaultConstructor == null || !Modifier.isPublic(defaultConstructor.getModifiers()) || !Modifier.isPublic(objectClass.getModifiers())) - || (defaultConstructor != null && defaultConstructor.getParameterCount() != 0)) { + || fieldBased && (defaultConstructor == null || !Modifier.isPublic(defaultConstructor.getModifiers()) || !Modifier.isPublic(objectClass.getModifiers()))) { MethodWriter mw = cw.visitMethod( Opcodes.ACC_PUBLIC, methodName, @@ -707,7 +706,10 @@ private static void newObject(MethodWriter mw, String TYPE_OBJECT, Constructor d mw.invokespecial(TYPE_OBJECT, "", "()V"); } else { Class paramType = defaultConstructor.getParameterTypes()[0]; - mw.aconst_null(); + mw.getstatic(TYPE_UNSAFE_UTILS, "UNSAFE", "Lsun/misc/Unsafe;"); + mw.visitLdcInsn(paramType); + mw.invokevirtual("sun/misc/Unsafe", "allocateInstance", "(Ljava/lang/Class;)Ljava/lang/Object;"); + mw.checkcast(ASMUtils.type(paramType)); mw.invokespecial(TYPE_OBJECT, "", "(" + ASMUtils.desc(paramType) + ")V"); } } @@ -2954,8 +2956,7 @@ private void genCreateObject( int objectModifiers = objectClass == null ? Modifier.PUBLIC : objectClass.getModifiers(); boolean publicObject = Modifier.isPublic(objectModifiers) && (objectClass == null || !classLoader.isExternalClass(objectClass)); - if (defaultConstructor == null || !publicObject || !Modifier.isPublic(defaultConstructor.getModifiers()) - || defaultConstructor.getParameterCount() != 0) { + if (defaultConstructor == null || !publicObject || !Modifier.isPublic(defaultConstructor.getModifiers())) { if (creator != null) { mw.aload(THIS); mw.getfield(classNameType, "creator", "Ljava/util/function/Supplier;"); diff --git a/core/src/test/java/com/alibaba/fastjson2/v1issues/issue_1000/Issue1082.java b/core/src/test/java/com/alibaba/fastjson2/v1issues/issue_1000/Issue1082.java index fa2e7dd852..0ee12fd659 100644 --- a/core/src/test/java/com/alibaba/fastjson2/v1issues/issue_1000/Issue1082.java +++ b/core/src/test/java/com/alibaba/fastjson2/v1issues/issue_1000/Issue1082.java @@ -1,7 +1,6 @@ package com.alibaba.fastjson2.v1issues.issue_1000; import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONException; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -15,15 +14,8 @@ public class Issue1082 { @Test public void test_for_issue() throws Exception { - Throwable error = null; - try { - Model_1082 m = (Model_1082) JSON.parseObject("{}", Model_1082.class); - } catch (JSONException ex) { - error = ex; - } catch (NullPointerException ex) { - error = ex; - } - assertNotNull(error); + Model_1082 m = (Model_1082) JSON.parseObject("{}", Model_1082.class); + assertNotNull(m); } public void f() { From 2791e091d99450e694db6536819a258a18dedddd Mon Sep 17 00:00:00 2001 From: wenshao Date: Tue, 28 Jul 2026 21:11:59 +0800 Subject: [PATCH 3/8] fix: address review comments on #7699 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ObjectReaderCreatorASM.newObject: only emit the UNSAFE.allocateInstance/checkcast sequence when the enclosing type is public. ldc/checkcast are access-checked against the generated reader, which lives in DynamicClassLoader, so a package-private enclosing type failed with IllegalAccessError (Issue1202) — the actual cause of the CI failures on this PR. - fastjson1-compatible Issue1082: assert a successful parse, matching the new behaviour. - ConstructorSupplier: cache the constructor parameter type instead of cloning getParameterTypes() on every get(). - ObjectReaderAdapter.createInstance: make the parameterCount == 1 assumption explicit and keep new Object[parameterCount] for constructors with 2+ parameters. - Issue7654: add a case whose inner class constructor dereferences the enclosing instance, so the fix is regression-gated on JDK < 25 as well. --- .../fastjson2/reader/ConstructorSupplier.java | 7 +++- .../fastjson2/reader/ObjectReaderAdapter.java | 4 +- .../reader/ObjectReaderCreatorASM.java | 15 ++++++-- .../fastjson2/issues_7000/Issue7654.java | 37 +++++++++++++++++++ .../fastjson/issue_1000/Issue1082.java | 10 +---- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java index fa001f7c8e..25baac025a 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java @@ -12,6 +12,7 @@ final class ConstructorSupplier final Constructor constructor; final Class objectClass; final boolean useClassNewInstance; + final Class paramType; public ConstructorSupplier(Constructor constructor) { constructor.setAccessible(true); @@ -20,6 +21,9 @@ public ConstructorSupplier(Constructor constructor) { this.useClassNewInstance = constructor.getParameterCount() == 0 && Modifier.isPublic(constructor.getModifiers()) && Modifier.isPublic(objectClass.getModifiers()); + this.paramType = constructor.getParameterCount() == 1 + ? constructor.getParameterTypes()[0] + : null; } @Override @@ -28,8 +32,7 @@ public Object get() { if (useClassNewInstance) { return objectClass.newInstance(); } else { - if (constructor.getParameterCount() == 1) { - Class paramType = constructor.getParameterTypes()[0]; + if (paramType != null) { Object dummy = JDKUtils.UNSAFE.allocateInstance(paramType); return constructor.newInstance(dummy); } else { diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java index 06478ebc37..e15ba5d8ed 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java @@ -447,10 +447,12 @@ public T createInstance(long features) { T object; if (parameterCount == 0) { object = (T) constructor.newInstance(); - } else { + } else if (parameterCount == 1) { Class paramType = constructor.getParameterTypes()[0]; Object dummy = JDKUtils.UNSAFE.allocateInstance(paramType); object = (T) constructor.newInstance(dummy); + } else { + object = (T) constructor.newInstance(new Object[parameterCount]); } if (hasDefaultValue) { initDefaultValue(object); diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java index f5b4a3c933..7374b22bd7 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java @@ -706,10 +706,17 @@ private static void newObject(MethodWriter mw, String TYPE_OBJECT, Constructor d mw.invokespecial(TYPE_OBJECT, "", "()V"); } else { Class paramType = defaultConstructor.getParameterTypes()[0]; - mw.getstatic(TYPE_UNSAFE_UTILS, "UNSAFE", "Lsun/misc/Unsafe;"); - mw.visitLdcInsn(paramType); - mw.invokevirtual("sun/misc/Unsafe", "allocateInstance", "(Ljava/lang/Class;)Ljava/lang/Object;"); - mw.checkcast(ASMUtils.type(paramType)); + // ldc/checkcast on the enclosing type are access-checked against the generated class, + // which lives in DynamicClassLoader; a non-public enclosing type fails with + // IllegalAccessError there, so keep passing null for it as before. + if (Modifier.isPublic(paramType.getModifiers())) { + mw.getstatic(TYPE_UNSAFE_UTILS, "UNSAFE", "Lsun/misc/Unsafe;"); + mw.visitLdcInsn(paramType); + mw.invokevirtual("sun/misc/Unsafe", "allocateInstance", "(Ljava/lang/Class;)Ljava/lang/Object;"); + mw.checkcast(ASMUtils.type(paramType)); + } else { + mw.aconst_null(); + } mw.invokespecial(TYPE_OBJECT, "", "(" + ASMUtils.desc(paramType) + ")V"); } } diff --git a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java index d992c29a2a..fbc6f62275 100644 --- a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java +++ b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java @@ -19,6 +19,43 @@ public void testNonStaticInnerClass() { assertEquals(0, result.getSummary().getMessagesInvalid()); } + /** + * The inner class constructor dereferences the enclosing instance, so a {@code null} + * {@code this$0} throws NPE on every JDK version. Unlike {@link #testNonStaticInnerClass()}, + * this gates the fix on JDK < 25 as well. + */ + @Test + public void testInnerClassConstructorDereferencingOuter() { + Outer.Inner inner = JSON.parseObject("{\"value\":123}", Outer.Inner.class); + assertNotNull(inner); + assertEquals(123, inner.getValue()); + } + + public static class Outer { + private String name = "outer"; + + public String name() { + return name; + } + + public class Inner { + private int value; + + public Inner() { + // invokevirtual on this$0: NPE if the enclosing instance is null + Outer.this.name(); + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + } + } + public static class AmzListingJsonFeedResult { private Summary summary; diff --git a/fastjson1-compatible/src/test/java/com/alibaba/fastjson/issue_1000/Issue1082.java b/fastjson1-compatible/src/test/java/com/alibaba/fastjson/issue_1000/Issue1082.java index 7c481e5398..d82c2aa1f9 100644 --- a/fastjson1-compatible/src/test/java/com/alibaba/fastjson/issue_1000/Issue1082.java +++ b/fastjson1-compatible/src/test/java/com/alibaba/fastjson/issue_1000/Issue1082.java @@ -1,7 +1,6 @@ package com.alibaba.fastjson.issue_1000; import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -12,13 +11,8 @@ public class Issue1082 { @Test public void test_for_issue() throws Exception { - Throwable error = null; - try { - Model_1082 m = (Model_1082) JSON.parseObject("{}", Model_1082.class); - } catch (JSONException | com.alibaba.fastjson2.JSONException | NullPointerException ex) { - error = ex; - } - assertNotNull(error); + Model_1082 m = (Model_1082) JSON.parseObject("{}", Model_1082.class); + assertNotNull(m); } public void f() { From fc16e351baeef6954e72943ef98488fd94e1fd9e Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 29 Jul 2026 00:00:24 +0800 Subject: [PATCH 4/8] fix: allocate the enclosing instance in ConstructorFunction too (#7654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With -Dfastjson2.creator=reflect a non-static inner class goes through beanInfo.creatorConstructor -> ObjectReaderNoneDefaultConstructor -> ConstructorFunction, which the previous commits did not cover: the synthetic enclosing-instance parameter never appears in the JSON, so it fell back to TypeUtils.getDefaultValue() and the constructor ran with a null this$0. ConstructorFunction now recognises that parameter and allocates a bare enclosing instance for it, matching ConstructorSupplier, ObjectReaderAdapter and the ASM path. Found by testInnerClassConstructorDereferencingOuter, which the ASM path passes and the reflect path did not — the Test Reflect CI jobs were failing on it. --- .../fastjson2/reader/ConstructorFunction.java | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java index c7ea0bbdb9..9076a9caa0 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java @@ -5,6 +5,7 @@ import com.alibaba.fastjson2.codec.FieldInfo; import com.alibaba.fastjson2.internal.asm.ASMUtils; import com.alibaba.fastjson2.util.Fnv; +import com.alibaba.fastjson2.util.JDKUtils; import com.alibaba.fastjson2.util.TypeUtils; import java.lang.reflect.*; @@ -23,6 +24,12 @@ final class ConstructorFunction final String[] paramNames; final boolean marker; final long[] hashCodes; + /** + * Non-null when parameter 0 is the synthetic enclosing instance of a non-static inner class. + * Such a parameter never appears in the JSON, so it must not be left null — the constructor + * may dereference it (JDK 25 rejects a null enclosing instance outright). + */ + final Class enclosingParamType; final List alternateConstructors; Map, Constructor> alternateConstructorMap; Map, String[]> alternateConstructorNames; @@ -57,6 +64,15 @@ final class ConstructorFunction hashCodes[i] = Fnv.hashCode64(name); } + Class declaringClass = constructor.getDeclaringClass(); + Class enclosingClass = declaringClass.getDeclaringClass(); + this.enclosingParamType = enclosingClass != null + && !Modifier.isStatic(declaringClass.getModifiers()) + && parameters.length > 0 + && parameters[0].getType() == enclosingClass + ? enclosingClass + : null; + this.alternateConstructors = alternateConstructors; if (alternateConstructors != null) { final int size = alternateConstructors.size(); @@ -99,6 +115,22 @@ final class ConstructorFunction } } + /** + * Default for a parameter absent from the JSON. Parameter 0 of a non-static inner class + * constructor is the enclosing instance, which is never present in the JSON, so allocate a + * bare one rather than passing null. + */ + private Object defaultArg(int index, Class paramClass) { + if (index == 0 && enclosingParamType != null) { + try { + return JDKUtils.UNSAFE.allocateInstance(enclosingParamType); + } catch (Throwable ignored) { + // fall back to null below + } + } + return TypeUtils.getDefaultValue(paramClass); + } + @Override public T apply(Map values) { boolean containsAll = true; @@ -139,7 +171,7 @@ public T apply(Map values) { Object arg = values.get(hashCodes[0]); Class paramType = param.getType(); if (arg == null) { - arg = TypeUtils.getDefaultValue(paramType); + arg = defaultArg(0, paramType); } else { if (!paramType.isInstance(arg)) { arg = TypeUtils.cast(arg, paramType); @@ -153,7 +185,7 @@ public T apply(Map values) { Parameter param0 = parameters[0]; Class param0Type = param0.getType(); if (arg0 == null) { - arg0 = TypeUtils.getDefaultValue(param0Type); + arg0 = defaultArg(0, param0Type); } else { if (!param0Type.isInstance(arg0)) { arg0 = TypeUtils.cast(arg0, param0Type); @@ -193,8 +225,8 @@ public T apply(Map values) { args[i] = arg; } else { flag |= (1 << i); - if (paramClass.isPrimitive()) { - args[i] = TypeUtils.getDefaultValue(paramClass); + if (paramClass.isPrimitive() || (i == 0 && enclosingParamType != null)) { + args[i] = defaultArg(i, paramClass); } } n = i + 1; @@ -210,7 +242,7 @@ public T apply(Map values) { Type paramType = parameter.getParameterizedType(); Object arg = values.get(hashCodes[i]); if (arg == null) { - arg = TypeUtils.getDefaultValue(paramClass); + arg = defaultArg(i, paramClass); } else { if (!paramClass.isInstance(arg)) { arg = TypeUtils.cast(arg, paramClass); From c7eeba75e06c39d11be70d462beff4f9cff4717a Mon Sep 17 00:00:00 2001 From: wenshao Date: Sun, 2 Aug 2026 15:58:50 +0800 Subject: [PATCH 5/8] fix: narrow the allocateInstance catch and cover both reader creators (#7654) - ConstructorFunction.defaultArg catches only InstantiationException, so an OutOfMemoryError from allocateInstance is no longer swallowed into a null enclosing instance, which would resurface as the NPE this PR fixes - Issue7654 now iterates TestUtils.readerCreators(), so the ConstructorFunction reflection path is covered as well as the ASM path --- .../fastjson2/reader/ConstructorFunction.java | 4 +-- .../fastjson2/issues_7000/Issue7654.java | 35 +++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java index 9076a9caa0..63d38bcff2 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java @@ -124,8 +124,8 @@ private Object defaultArg(int index, Class paramClass) { if (index == 0 && enclosingParamType != null) { try { return JDKUtils.UNSAFE.allocateInstance(enclosingParamType); - } catch (Throwable ignored) { - // fall back to null below + } catch (InstantiationException ignored) { + // the enclosing type cannot be allocated, fall back to the default value below } } return TypeUtils.getDefaultValue(paramClass); diff --git a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java index fbc6f62275..0fc5f15fff 100644 --- a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java +++ b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java @@ -1,6 +1,10 @@ package com.alibaba.fastjson2.issues_7000; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.TestUtils; +import com.alibaba.fastjson2.reader.ObjectReader; +import com.alibaba.fastjson2.reader.ObjectReaderCreator; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -10,13 +14,23 @@ public class Issue7654 { @Test public void testNonStaticInnerClass() { String json = "{\"issues\":[],\"summary\":{\"errors\":0,\"warnings\":0,\"messagesProcessed\":7,\"messagesAccepted\":7,\"messagesInvalid\":0}}"; - AmzListingJsonFeedResult result = JSON.parseObject(json, AmzListingJsonFeedResult.class); - assertNotNull(result.getSummary()); - assertEquals(7, result.getSummary().getMessagesAccepted()); - assertEquals(7, result.getSummary().getMessagesProcessed()); - assertEquals(0, result.getSummary().getErrors()); - assertEquals(0, result.getSummary().getWarnings()); - assertEquals(0, result.getSummary().getMessagesInvalid()); + assertSummary(JSON.parseObject(json, AmzListingJsonFeedResult.class).getSummary()); + + String summaryJson = "{\"errors\":0,\"warnings\":0,\"messagesProcessed\":7,\"messagesAccepted\":7,\"messagesInvalid\":0}"; + for (ObjectReaderCreator creator : TestUtils.readerCreators()) { + ObjectReader objectReader + = creator.createObjectReader(AmzListingJsonFeedResult.Summary.class); + assertSummary(objectReader.readObject(JSONReader.of(summaryJson), 0)); + } + } + + private static void assertSummary(AmzListingJsonFeedResult.Summary summary) { + assertNotNull(summary); + assertEquals(7, summary.getMessagesAccepted()); + assertEquals(7, summary.getMessagesProcessed()); + assertEquals(0, summary.getErrors()); + assertEquals(0, summary.getWarnings()); + assertEquals(0, summary.getMessagesInvalid()); } /** @@ -29,6 +43,13 @@ public void testInnerClassConstructorDereferencingOuter() { Outer.Inner inner = JSON.parseObject("{\"value\":123}", Outer.Inner.class); assertNotNull(inner); assertEquals(123, inner.getValue()); + + for (ObjectReaderCreator creator : TestUtils.readerCreators()) { + ObjectReader objectReader = creator.createObjectReader(Outer.Inner.class); + Outer.Inner value = objectReader.readObject(JSONReader.of("{\"value\":123}"), 0); + assertNotNull(value); + assertEquals(123, value.getValue()); + } } public static class Outer { From 326b5104ece9f4bf29d9a4320e610ea5312047f4 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Mon, 3 Aug 2026 16:08:35 +0800 Subject: [PATCH 6/8] fix: handle abstract and non-public enclosing classes for inner class instantiation (#7654) --- .../fastjson2/reader/ConstructorFunction.java | 15 +++++------ .../fastjson2/reader/ConstructorSupplier.java | 27 ++++++++++++------- .../fastjson2/reader/ObjectReaderAdapter.java | 11 ++++++-- .../reader/ObjectReaderCreatorASM.java | 26 +++++++++++++++--- .../com/alibaba/fastjson2/util/BeanUtils.java | 17 ++++++++++++ 5 files changed, 72 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java index 63d38bcff2..cb0d1c443f 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorFunction.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONFactory; import com.alibaba.fastjson2.codec.FieldInfo; import com.alibaba.fastjson2.internal.asm.ASMUtils; +import com.alibaba.fastjson2.util.BeanUtils; import com.alibaba.fastjson2.util.Fnv; import com.alibaba.fastjson2.util.JDKUtils; import com.alibaba.fastjson2.util.TypeUtils; @@ -64,14 +65,7 @@ final class ConstructorFunction hashCodes[i] = Fnv.hashCode64(name); } - Class declaringClass = constructor.getDeclaringClass(); - Class enclosingClass = declaringClass.getDeclaringClass(); - this.enclosingParamType = enclosingClass != null - && !Modifier.isStatic(declaringClass.getModifiers()) - && parameters.length > 0 - && parameters[0].getType() == enclosingClass - ? enclosingClass - : null; + this.enclosingParamType = BeanUtils.getEnclosingInstanceParamType(constructor); this.alternateConstructors = alternateConstructors; if (alternateConstructors != null) { @@ -118,7 +112,10 @@ final class ConstructorFunction /** * Default for a parameter absent from the JSON. Parameter 0 of a non-static inner class * constructor is the enclosing instance, which is never present in the JSON, so allocate a - * bare one rather than passing null. + * bare one rather than passing null. When the enclosing class cannot be allocated (for + * example an abstract class), fall back to null as before JDK 25; inner classes compiled + * by JDK 25+ then reject the null enclosing instance, which is unavoidable because an + * abstract enclosing class cannot be instantiated. */ private Object defaultArg(int index, Class paramClass) { if (index == 0 && enclosingParamType != null) { diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java index 25baac025a..6b8a0e5141 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ConstructorSupplier.java @@ -1,6 +1,7 @@ package com.alibaba.fastjson2.reader; import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.util.BeanUtils; import com.alibaba.fastjson2.util.JDKUtils; import java.lang.reflect.Constructor; @@ -21,9 +22,7 @@ public ConstructorSupplier(Constructor constructor) { this.useClassNewInstance = constructor.getParameterCount() == 0 && Modifier.isPublic(constructor.getModifiers()) && Modifier.isPublic(objectClass.getModifiers()); - this.paramType = constructor.getParameterCount() == 1 - ? constructor.getParameterTypes()[0] - : null; + this.paramType = BeanUtils.getEnclosingInstanceParamType(constructor); } @Override @@ -31,14 +30,24 @@ public Object get() { try { if (useClassNewInstance) { return objectClass.newInstance(); - } else { - if (paramType != null) { - Object dummy = JDKUtils.UNSAFE.allocateInstance(paramType); - return constructor.newInstance(dummy); - } else { - return constructor.newInstance(); + } + + if (paramType != null) { + Object dummy; + try { + dummy = JDKUtils.UNSAFE.allocateInstance(paramType); + } catch (InstantiationException ignored) { + // the enclosing class cannot be allocated (for example an abstract class), + // pass null as before JDK 25 + dummy = null; } + return constructor.newInstance(dummy); + } + + if (constructor.getParameterCount() == 1) { + return constructor.newInstance(new Object[1]); } + return constructor.newInstance(); } catch (Throwable e) { throw new JSONException("create instance error", e); } diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java index e15ba5d8ed..623c755fd4 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderAdapter.java @@ -447,9 +447,16 @@ public T createInstance(long features) { T object; if (parameterCount == 0) { object = (T) constructor.newInstance(); - } else if (parameterCount == 1) { + } else if (parameterCount == 1 && BeanUtils.getEnclosingInstanceParamType(constructor) != null) { Class paramType = constructor.getParameterTypes()[0]; - Object dummy = JDKUtils.UNSAFE.allocateInstance(paramType); + Object dummy; + try { + dummy = JDKUtils.UNSAFE.allocateInstance(paramType); + } catch (InstantiationException ignored) { + // the enclosing class cannot be allocated (for example an abstract class), + // pass null as before JDK 25 + dummy = null; + } object = (T) constructor.newInstance(dummy); } else { object = (T) constructor.newInstance(new Object[parameterCount]); diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java index 7374b22bd7..72f2c401e0 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java @@ -633,7 +633,12 @@ private ObjectReaderBean jitObjectReader( mw.invokevirtual("sun/misc/Unsafe", "allocateInstance", "(Ljava/lang/Class;)Ljava/lang/Object;"); mw.areturn(); mw.visitMaxs(3, 3); - } else if (defaultConstructor != null && Modifier.isPublic(defaultConstructor.getModifiers()) && Modifier.isPublic(objectClass.getModifiers())) { + } else if (defaultConstructor != null + && Modifier.isPublic(defaultConstructor.getModifiers()) + && Modifier.isPublic(objectClass.getModifiers()) + && enclosingTypeVisible(defaultConstructor)) { + // a non-public enclosing type cannot be referenced from the generated class, + // so skip the override and let the reflective ObjectReaderAdapter.createInstance handle it MethodWriter mw = cw.visitMethod( Opcodes.ACC_PUBLIC, methodName, @@ -708,8 +713,10 @@ private static void newObject(MethodWriter mw, String TYPE_OBJECT, Constructor d Class paramType = defaultConstructor.getParameterTypes()[0]; // ldc/checkcast on the enclosing type are access-checked against the generated class, // which lives in DynamicClassLoader; a non-public enclosing type fails with - // IllegalAccessError there, so keep passing null for it as before. - if (Modifier.isPublic(paramType.getModifiers())) { + // IllegalAccessError there. An abstract enclosing type cannot be allocated via + // Unsafe.allocateInstance (it throws InstantiationException at runtime). In both + // cases keep passing null as before JDK 25. + if (Modifier.isPublic(paramType.getModifiers()) && !Modifier.isAbstract(paramType.getModifiers())) { mw.getstatic(TYPE_UNSAFE_UTILS, "UNSAFE", "Lsun/misc/Unsafe;"); mw.visitLdcInsn(paramType); mw.invokevirtual("sun/misc/Unsafe", "allocateInstance", "(Ljava/lang/Class;)Ljava/lang/Object;"); @@ -721,6 +728,16 @@ private static void newObject(MethodWriter mw, String TYPE_OBJECT, Constructor d } } + /** + * Whether the enclosing type referenced by the given inner class constructor can be loaded + * via {@code ldc}/{@code checkcast} from a generated reader class. A non-public enclosing + * type cannot, so instance creation must be delegated to the reflective creator instead. + */ + private static boolean enclosingTypeVisible(Constructor constructor) { + return constructor.getParameterCount() == 0 + || Modifier.isPublic(constructor.getParameterTypes()[0].getModifiers()); + } + private void genMethodGetFieldReader(ObjectReadContext context) { ObjectReaderAdapter objectReaderAdapter = context.objectReaderAdapter; genMethodGetFieldReaderImpl( @@ -2963,7 +2980,8 @@ private void genCreateObject( int objectModifiers = objectClass == null ? Modifier.PUBLIC : objectClass.getModifiers(); boolean publicObject = Modifier.isPublic(objectModifiers) && (objectClass == null || !classLoader.isExternalClass(objectClass)); - if (defaultConstructor == null || !publicObject || !Modifier.isPublic(defaultConstructor.getModifiers())) { + if (defaultConstructor == null || !publicObject || !Modifier.isPublic(defaultConstructor.getModifiers()) + || !enclosingTypeVisible(defaultConstructor)) { if (creator != null) { mw.aload(THIS); mw.getfield(classNameType, "creator", "Ljava/util/function/Supplier;"); diff --git a/core/src/main/java/com/alibaba/fastjson2/util/BeanUtils.java b/core/src/main/java/com/alibaba/fastjson2/util/BeanUtils.java index 1e27cf4ec1..3f6a168853 100644 --- a/core/src/main/java/com/alibaba/fastjson2/util/BeanUtils.java +++ b/core/src/main/java/com/alibaba/fastjson2/util/BeanUtils.java @@ -2624,6 +2624,23 @@ public static void processJacksonJsonIgnore(FieldInfo fieldInfo, Annotation anno }); } + /** + * Returns the enclosing class if the given constructor is a non-static member class + * constructor whose first parameter is the enclosing instance ({@code this$0}), + * otherwise returns null. + */ + public static Class getEnclosingInstanceParamType(Constructor constructor) { + Class declaringClass = constructor.getDeclaringClass(); + Class enclosingClass = declaringClass.getDeclaringClass(); + if (enclosingClass != null + && !Modifier.isStatic(declaringClass.getModifiers()) + && constructor.getParameterCount() > 0 + && constructor.getParameterTypes()[0] == enclosingClass) { + return enclosingClass; + } + return null; + } + public static boolean isNoneStaticMemberClass(Class objectClass, Class memberClass) { if (memberClass == null || memberClass.isPrimitive() From 3e4929dd04484da6c82f7e7387cf522b12b99927 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Mon, 3 Aug 2026 16:25:28 +0800 Subject: [PATCH 7/8] fix: delegate inner class creator-constructor readers to reflective path, add tests (#7654) --- .../reader/ObjectReaderCreatorASM.java | 5 + .../fastjson2/issues_7000/Issue7654.java | 150 ++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java index 72f2c401e0..ddd52b4d2b 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java @@ -399,6 +399,11 @@ protected ObjectReaderNoneDefaultConstructor createNoneDefaultConstructorObj || (objectReaderAdapter.noneDefaultConstructor != null && objectReaderAdapter.noneDefaultConstructor.getParameterCount() != paramFieldReaders.length) || (constructorFunction instanceof FactoryFunction && ((FactoryFunction) constructorFunction).paramNames.length != paramFieldReaders.length) || paramFieldReaders.length > 64 + // a non-static inner class constructor carries the synthetic enclosing instance + // parameter, which the generated reader cannot allocate; use the reflective + // ConstructorFunction that allocates it + || (objectReaderAdapter.noneDefaultConstructor != null + && BeanUtils.getEnclosingInstanceParamType(objectReaderAdapter.noneDefaultConstructor) != null) ) { match = false; } diff --git a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java index 0fc5f15fff..5554556512 100644 --- a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java +++ b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7654.java @@ -52,6 +52,81 @@ public void testInnerClassConstructorDereferencingOuter() { } } + /** + * A non-static inner class whose constructor declares one explicit parameter in addition to + * the synthetic enclosing instance (two parameters total), which routes through the + * {@code BiFunction} fast path of {@code ConstructorFunction.apply}. The constructor + * dereferences the enclosing instance, so a {@code null} {@code this$0} throws on every JDK. + */ + @Test + public void testInnerClassWithExplicitConstructorParameter() { + String json = "{\"id\":123}"; + for (ObjectReaderCreator creator : TestUtils.readerCreators()) { + ObjectReader objectReader + = creator.createObjectReader(Outer2.InnerWithOneParam.class); + Outer2.InnerWithOneParam value = objectReader.readObject(JSONReader.of(json), 0); + assertNotNull(value); + assertEquals(123, value.getId()); + } + } + + /** + * A non-static inner class whose constructor declares two explicit parameters in addition to + * the synthetic enclosing instance (three parameters total), which routes through the general + * argument loop of {@code ConstructorFunction.apply} rather than the single/dual fast paths. + * The constructor dereferences the enclosing instance, so a {@code null} {@code this$0} throws + * on every JDK. + */ + @Test + public void testInnerClassWithTwoExplicitConstructorParameters() { + String json = "{\"id\":123,\"name\":\"fastjson2\"}"; + for (ObjectReaderCreator creator : TestUtils.readerCreators()) { + ObjectReader objectReader + = creator.createObjectReader(Outer2.InnerWithTwoParams.class); + Outer2.InnerWithTwoParams value = objectReader.readObject(JSONReader.of(json), 0); + assertNotNull(value); + assertEquals(123, value.getId()); + assertEquals("fastjson2", value.getName()); + } + } + + /** + * The enclosing class is abstract, so it cannot be allocated via + * {@code Unsafe.allocateInstance} (which throws {@link InstantiationException}). The readers + * must fall back to passing {@code null} for the enclosing instance instead of propagating the + * exception (the pre-JDK-25 behavior). This module is compiled for Java 8, so the inner class + * constructor does not reject a {@code null} enclosing instance. + */ + @Test + public void testAbstractEnclosingClass() { + String json = "{\"value\":42}"; + for (ObjectReaderCreator creator : TestUtils.readerCreators()) { + ObjectReader objectReader + = creator.createObjectReader(AbstractOuter.Inner.class); + AbstractOuter.Inner value = objectReader.readObject(JSONReader.of(json), 0); + assertNotNull(value); + assertEquals(42, value.getValue()); + } + } + + /** + * The enclosing class is package-private, so a generated ASM reader cannot reference it via + * {@code ldc}/{@code checkcast} and instance creation is delegated to the reflective creator. + * The inner constructor dereferences the enclosing instance, so a {@code null} {@code this$0} + * throws on every JDK. + */ + @Test + public void testPackagePrivateEnclosingClass() { + String json = "{\"value\":42}"; + for (ObjectReaderCreator creator : TestUtils.readerCreators()) { + ObjectReader objectReader + = creator.createObjectReader(PackagePrivateOuter.Inner.class); + PackagePrivateOuter.Inner value = objectReader.readObject(JSONReader.of(json), 0); + assertNotNull(value); + assertEquals(42, value.getValue()); + } + } + public static class Outer { private String name = "outer"; @@ -136,4 +211,79 @@ public void setMessagesInvalid(Integer messagesInvalid) { } } } + + public static class Outer2 { + private String name = "outer2"; + + public String name() { + return name; + } + + public class InnerWithOneParam { + private final int id; + + public InnerWithOneParam(int id) { + // invokevirtual on this$0: NPE if the enclosing instance is null + Outer2.this.name(); + this.id = id; + } + + public int getId() { + return id; + } + } + + public class InnerWithTwoParams { + private final int id; + private final String name; + + public InnerWithTwoParams(int id, String name) { + // invokevirtual on this$0: NPE if the enclosing instance is null + Outer2.this.name(); + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + } + } + + public abstract static class AbstractOuter { + public class Inner { + private int value; + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + } + } + + static class PackagePrivateOuter { + public class Inner { + private int value; + + public Inner() { + // invokevirtual on this$0: NPE if the enclosing instance is null + PackagePrivateOuter.this.toString(); + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + } + } } From 43cf858073f4e6c9662d879333ec6ceffaa2b278 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Mon, 3 Aug 2026 16:31:31 +0800 Subject: [PATCH 8/8] test: add JDK 25 regression test for inner class requireNonNull(this$0) (#7654) --- .../com/alibaba/fastjson2/Issue7654Test.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test-jdk25/src/test/java/com/alibaba/fastjson2/Issue7654Test.java diff --git a/test-jdk25/src/test/java/com/alibaba/fastjson2/Issue7654Test.java b/test-jdk25/src/test/java/com/alibaba/fastjson2/Issue7654Test.java new file mode 100644 index 0000000000..af16f5b722 --- /dev/null +++ b/test-jdk25/src/test/java/com/alibaba/fastjson2/Issue7654Test.java @@ -0,0 +1,29 @@ +package com.alibaba.fastjson2; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * On JDK 25 the compiler emits {@code Objects.requireNonNull(this$0)} at the top of every + * non-static inner class constructor. Deserializing such a class therefore requires a non-null + * enclosing instance to be supplied to the constructor; passing {@code null} (as fastjson2 did + * previously) makes the constructor throw {@link NullPointerException}. This module is compiled + * with {@code maven.compiler.source/target=25}, so the inner class constructor below carries the + * {@code requireNonNull} check and reproduces issue #7654 directly. + */ +public class Issue7654Test { + @Test + public void testInnerClass() { + Outer.Inner bean = JSON.parseObject("{\"id\":123}", Outer.Inner.class); + assertNotNull(bean); + assertEquals(123, bean.id); + } + + public static class Outer { + public class Inner { + public int id; + } + } +}