question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Java 14/15 records can not set final field

See original GitHub issue

Hi, as the following error describes, java records (preview feature) deserialization isn’t working in gson, Jackson is adding support in its very soon release 2.12, is there going to be same support/fix in gson ?

java.lang.AssertionError: AssertionError (GSON 2.8.6): java.lang.IllegalAccessException: Can not set final java.lang.String field JsonGsonRecordTest$Emp.name to java.lang.String

Here’s a sample test

  record Emp(String name) {}

  @Test
  void deserializeEngineer() {
    Gson j = new GsonBuilder().setPrettyPrinting().create();
    var empJson = """
            {
              "name": "bob"
            }""";
    var empObj = j.fromJson(empJson, Emp.class);
  }

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:30
  • Comments:25 (3 by maintainers)

github_iconTop GitHub Comments

12reactions
sceutrecommented, May 9, 2021

Another workaround would be to use a factory so you don’t have to write deserializers for each record.

package com.smeethes.server;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

public class RecordsWithGson {

   public static class RecordTypeAdapterFactory implements TypeAdapterFactory {

      @Override
      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
         @SuppressWarnings("unchecked")
         Class<T> clazz = (Class<T>) type.getRawType();
         if (!clazz.isRecord()) {
            return null;
         }
         TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);

         return new TypeAdapter<T>() {
            @Override
            public void write(JsonWriter out, T value) throws IOException {
               delegate.write(out, value);
            }

            @Override
            public T read(JsonReader reader) throws IOException {
               if (reader.peek() == JsonToken.NULL) {
                  reader.nextNull();
                  return null;
               } else {
                  var recordComponents = clazz.getRecordComponents();
                  var typeMap = new HashMap<String,TypeToken<?>>();
                  for (int i = 0; i < recordComponents.length; i++) {
                     typeMap.put(recordComponents[i].getName(), TypeToken.get(recordComponents[i].getGenericType()));
                  }
                  var argsMap = new HashMap<String,Object>();
                  reader.beginObject();
                  while (reader.hasNext()) {
                     String name = reader.nextName();
                     argsMap.put(name, gson.getAdapter(typeMap.get(name)).read(reader));
                  }
                  reader.endObject();

                  var argTypes = new Class<?>[recordComponents.length];
                  var args = new Object[recordComponents.length];
                  for (int i = 0; i < recordComponents.length; i++) {
                     argTypes[i] = recordComponents[i].getType();
                     args[i] = argsMap.get(recordComponents[i].getName());
                  }
                  Constructor<T> constructor;
                  try {
                     constructor = clazz.getDeclaredConstructor(argTypes);
                     constructor.setAccessible(true);
                     return constructor.newInstance(args);
                  } catch (NoSuchMethodException | InstantiationException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                     throw new RuntimeException(e);
                  }
               }
            }
         };
      }
   }
   
   private record FooA(int a) {}
   private record FooB(int b) {}
   private record Bar(FooA fooA, FooB fooB, int bar) {}
   private class AClass {
      String data;
      Bar bar;
      public String toString() { return "AClass [data=" + data + ", bar=" + bar + "]"; }
   }
   
   public static void main(String[] args) {
      var gson = new GsonBuilder()
            .registerTypeAdapterFactory(new RecordTypeAdapterFactory())
            .create();
      var text = """
      { 
         "data": "some data",
         "bar": {
            "fooA": { "a": 1 },
            "fooB": { "b": 2 },
            "bar": 3
         }
      }
      """;
      
      AClass a = gson.fromJson(text, AClass.class);
      System.out.println(a);
   }
}
10reactions
Baizleycommented, Nov 8, 2020

The problem seems to exist only in Java 15 with preview enabled. Running your test under Java 14 with preview enabled and printing the empObj gives Emp[name=bob].

JDKs used,

  1. openjdk-14.0.2_linux-x64_bin
  2. openjdk-15_linux-x64_bin

This is due to changes in Java 15 that makes final fields in records notmodifiable via reflection. More information can be found here:

  1. (15) RFR: JDK-8247444: Trust final fields in records
  2. JDK-8247444 - Trust final fields in records

The relevant part on handling them going forward:

This change impacts 3rd-party frameworks including 3rd-party serialization framework that rely on core reflection setAccessible or sun.misc.Unsafe::allocateInstance and objectFieldOffset etc to construct records but not using the canonical constructor. These frameworks would need to be updated to construct records via its canonical constructor as done by the Java serialization.

I see this change gives a good opportunity to engage the maintainers of the serialization frameworks and work together to support new features including records, inline classes and the new serialization mechanism and which I think it is worth the investment.

A current workaround is to write a Deserializers that uses the records constructor instead of reflection.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Cannot use final fields in Room entity - java - Stack Overflow
I have created a similar class called Record, following the official docs. public class Record { @PrimaryKey(autoGenerate = true) @ColumnInfo( ...
Read more >
4 Records - Java - Oracle Help Center
Records cannot declare instance fields (other than the private final fields that correspond to the components of the record component list); any other...
Read more >
JDK-8247444 Trust final fields in records - Java Bug Database
No change in Field::setAccessible(true), i.e. it will succeed to allow existing frameworks to have read access to final fields in records (no write...
Read more >
Font0 - Rocket Software Documentation
Using Structs for Disconnected Record Sets ... Create a Java Keystore ... Model refers to input field FieldName, which is not found in...
Read more >
2 Server Error Message Reference - MySQL :: Developer Zone
Message: Record has changed since last read in table '%s' ... Message: Variable '%s' is a SESSION variable and can't be used with...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found