From 1b928c43f1d164a196ec6df6b8a17bf0997e9054 Mon Sep 17 00:00:00 2001 From: Utsav Date: Thu, 12 Mar 2026 15:15:25 +0530 Subject: [PATCH 1/4] Introduce dedicated error handling for final-field mutation during deserialization (JEP 500) --- .../bind/ReflectiveTypeAdapterFactory.java | 10 +++++- .../internal/reflect/ReflectionHelper.java | 35 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index ef464684aa..4960af6ed3 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -279,7 +279,15 @@ void readIntoField(JsonReader reader, Object target) String fieldDescription = ReflectionHelper.getAccessibleObjectDescription(field, false); throw new JsonIOException("Cannot set value of 'static final' " + fieldDescription); } - field.set(target, fieldValue); + try { + field.set(target, fieldValue); + } catch (IllegalAccessException e) { + // If the field is final, provide a dedicated, clearer error message. + if (Modifier.isFinal(field.getModifiers())) { + throw ReflectionHelper.createExceptionForFinalFieldMutation(field, e); + } + throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); + } } } }; diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 0452942260..54e948daeb 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -206,6 +206,41 @@ public static RuntimeException createExceptionForUnexpectedIllegalAccess( exception); } + /** + * Creates a {@link JsonIOException} indicating that Gson was unable to set a {@code final} + * instance field via reflection during deserialization. + * + *

This helper is used when {@link Field#set(Object, Object)} throws an + * {@link IllegalAccessException} for a {@code final} field. The returned exception message aims + * to be actionable by explaining that Gson cannot mutate final instance fields and suggesting + * alternatives such as registering a custom {@code TypeAdapter} or {@code InstanceCreator}, or + * making the field non-final. + * + *

On newer Java runtimes, reflective mutation of {@code final} fields may be restricted (see + * JEP 500). Depending on JVM configuration, attempts to mutate final fields reflectively can + * cause {@link IllegalAccessException}s. + * + * @param field The final field which Gson attempted to assign + * @param exception The {@link IllegalAccessException} thrown by {@link Field#set(Object, Object)} + * @return A {@link JsonIOException} wrapping the original {@code exception} + */ + public static JsonIOException createExceptionForFinalFieldMutation( + Field field, IllegalAccessException exception) { + String fieldDescription = getAccessibleObjectDescription(field, false); + return new com.google.gson.JsonIOException( + "Cannot set value of final " + fieldDescription + ".\n" + + "Gson cannot modify final instance fields during deserialization. Register a" + + " TypeAdapter or InstanceCreator for the declaring type, or change the field to be" + + " non-final.\n" + + "Recent Java runtimes increasingly restrict reflective final-field mutation (JEP 500," + + " \"Prepare to Make Final Mean Final\"). If you are running on JDK 26+ and need to" + + " allow it, configure the JVM accordingly (for example" + + " --enable-final-field-mutation=ALL-UNNAMED or your module, and" + + " --illegal-final-field-mutation=allow/warn/debug/deny). See" + + " https://openjdk.org/jeps/500", + exception); + } + private static RuntimeException createExceptionForRecordReflectionException( ReflectiveOperationException exception) { throw new RuntimeException( From 9969bf56a13e85e010bd49bdf1457ebf73f6db11 Mon Sep 17 00:00:00 2001 From: Utsav Date: Thu, 12 Mar 2026 21:45:20 +0530 Subject: [PATCH 2/4] Unit test for createExceptionForFinalFieldMutation custom error handler --- ...ctiveTypeAdapterFactoryFinalFieldTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryFinalFieldTest.java diff --git a/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryFinalFieldTest.java b/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryFinalFieldTest.java new file mode 100644 index 0000000000..9bae9da678 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryFinalFieldTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2026 The Gson Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson.internal.bind; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertSame; + +import com.google.gson.JsonIOException; +import com.google.gson.internal.reflect.ReflectionHelper; +import java.lang.reflect.Field; +import org.junit.Test; + +/** Tests for the helper used by {@link ReflectiveTypeAdapterFactory} for final-field mutation. */ +public final class ReflectiveTypeAdapterFactoryFinalFieldTest { + + private static final class ClassWithFinalField { + @SuppressWarnings("unused") + final String finalField; + + ClassWithFinalField(String finalField) { + this.finalField = finalField; + } + } + + @Test + public void createExceptionForFinalFieldMutation_includesHelpfulMessageAndJepLink() + throws NoSuchFieldException { + Field finalField = ClassWithFinalField.class.getDeclaredField("finalField"); + IllegalAccessException cause = new IllegalAccessException("test"); + + JsonIOException exception = + ReflectionHelper.createExceptionForFinalFieldMutation(finalField, cause); + + assertThat(exception).hasMessageThat().contains("Cannot set value of final"); + assertThat(exception).hasMessageThat().contains("https://openjdk.org/jeps/500"); + assertSame(cause, exception.getCause()); + } +} From 63d8bc3968db15c256e48e9b099384f80be1e578 Mon Sep 17 00:00:00 2001 From: Utsav Date: Thu, 12 Mar 2026 21:45:59 +0530 Subject: [PATCH 3/4] Miscellaneous formatting fix --- .../gson/internal/reflect/ReflectionHelper.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 54e948daeb..9150020e60 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -210,14 +210,14 @@ public static RuntimeException createExceptionForUnexpectedIllegalAccess( * Creates a {@link JsonIOException} indicating that Gson was unable to set a {@code final} * instance field via reflection during deserialization. * - *

This helper is used when {@link Field#set(Object, Object)} throws an - * {@link IllegalAccessException} for a {@code final} field. The returned exception message aims - * to be actionable by explaining that Gson cannot mutate final instance fields and suggesting + *

This helper is used when {@link Field#set(Object, Object)} throws an {@link + * IllegalAccessException} for a {@code final} field. The returned exception message aims to be + * actionable by explaining that Gson cannot mutate final instance fields and suggesting * alternatives such as registering a custom {@code TypeAdapter} or {@code InstanceCreator}, or * making the field non-final. * *

On newer Java runtimes, reflective mutation of {@code final} fields may be restricted (see - * JEP 500). Depending on JVM configuration, attempts to mutate final fields reflectively can + * JEP 500). Depending on JVM configuration, attempts to mutate final fields reflectively can * cause {@link IllegalAccessException}s. * * @param field The final field which Gson attempted to assign @@ -228,7 +228,9 @@ public static JsonIOException createExceptionForFinalFieldMutation( Field field, IllegalAccessException exception) { String fieldDescription = getAccessibleObjectDescription(field, false); return new com.google.gson.JsonIOException( - "Cannot set value of final " + fieldDescription + ".\n" + "Cannot set value of final " + + fieldDescription + + ".\n" + "Gson cannot modify final instance fields during deserialization. Register a" + " TypeAdapter or InstanceCreator for the declaring type, or change the field to be" + " non-final.\n" From e337dd0a0a78d37c7895da69fd5a1ac15a426e4b Mon Sep 17 00:00:00 2001 From: Utsav Date: Fri, 13 Mar 2026 00:25:20 +0530 Subject: [PATCH 4/4] Remove unnecessary fully qualified name for JsonIOException --- .../java/com/google/gson/internal/reflect/ReflectionHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 9150020e60..c3f609391c 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -227,7 +227,7 @@ public static RuntimeException createExceptionForUnexpectedIllegalAccess( public static JsonIOException createExceptionForFinalFieldMutation( Field field, IllegalAccessException exception) { String fieldDescription = getAccessibleObjectDescription(field, false); - return new com.google.gson.JsonIOException( + return new JsonIOException( "Cannot set value of final " + fieldDescription + ".\n"