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..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 @@ -206,6 +206,43 @@ 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 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( 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()); + } +}