Gson deserialization: set final field

14,280

Solution 1

Gson uses reflection to set final fields (via .setAccessible(true)), so the problem you describe (and other related) probably comes from the way Java handles finals...

JLS 17.5.3 Subsequent Modification of final Fields

In some cases, such as deserialization, the system will need to change the final fields of an object after construction. final fields can be changed via reflection and other implementation-dependent means. The only pattern in which this has reasonable semantics is one in which an object is constructed and then the final fields of the object are updated. The object should not be made visible to other threads, nor should the final fields be read, until all updates to the final fields of the object are complete. Freezes of a final field occur both at the end of the constructor in which the final field is set, and immediately after each modification of a final field via reflection or other special mechanism.

Even then, there are a number of complications. If a final field is initialized to a compile-time constant expression (§15.28) in the field declaration, changes to the final field may not be observed, since uses of that final field are replaced at compile time with the value of the constant expression.

Another problem is that the specification allows aggressive optimization of final fields. Within a thread, it is permissible to reorder reads of a final field with those modifications of a final field that do not take place in the constructor.

Solution 2

It's a little difficult to troubleshoot the exact problem in the original question, since complete minimal code and examples were not provided. Perhaps the following understanding of how Gson instantiates target objects during deserialization helps.

Gson 1.7.1 and 2.0 both respect final field assignments during vanilla deserialization operations, and the initial final field assignments are not altered. For example:

import com.google.gson.Gson;

public class GsonFoo
{
  public static void main(String[] args)
  {
    // {"name":"Fred","id":42}
    String json1 = "{\"name\":\"Fred\",\"id\":42}";
    System.out.println(new Gson().fromJson(json1, Bar1.class));
    // output:
    // Bar1: name=Fred, id=-1
  }
}

class Bar1
{
  String name = "BLANK";
  final int id = -1;

  @Override
  public String toString()
  {
    return String.format("Bar1: name=%s, id=%d", name, id);
  }
}

On the other hand, for instance creation during deserialization, since Gson uses sun.misc.Unsafe -- and not a user-defined constructor -- assignments of final fields explicitly defined in any constructor are not respected. For example:

import com.google.gson.Gson;

public class GsonFoo
{
  public static void main(String[] args)
  {
    // {"name":"Fred","id":42}
    String json1 = "{\"name\":\"Fred\",\"id\":42}";
    System.out.println(new Gson().fromJson(json1, Bar1.class));
    // output:
    // Bar1: name=Fred, id=42
  }
}

class Bar1
{
  String name = "BLANK";
  final int id;

  Bar1()
  {
    id = -1;
  }

  @Override
  public String toString()
  {
    return String.format("Bar1: name=%s, id=%d", name, id);
  }
}

In summary, during vanilla deserialization, it's not possible to reassign a final field assignment with any data from the incoming JSON, though it may appear to be possible if the final field assignment is occurring in a user-defined constructor.**

**I'm not sure if the JVM spec allows for some implementation leniency such that different behavior than what's described above might be observed when running on different JVMs.

Solution 3

The specificity of the question doesn't quite match the generic title, but I had the same problem and the question is a top-rated Google result for me, so I'll respond here despite the question's age.

To summarise the findings of the other answers here, it seems that past and current versions of Gson will deserialise objects but will not deserialise primitives if they have been field-initialised.

public final class A {
    // Gson will properly deserialise foo, regardless of initialisation.
    public final String foo = "bar";
}
public final class B {
    // i will always be 42.
    public final int i = 42;
}
public final class C {
    // Gson will properly deserialise j
    public final int j;
    public C() { j = 37; }
}

The various inconsistencies in this behaviour was enough for me to decide to define custom type adapters and omit the default constructors. Since Gson 2.1, TypeAdapter makes this very simple. The downside is that Gson now can't handle this automatically.

Given a type Point defined as:

public final class Point {
    public final int x;
    public final int y;
    public Point(final int x, final int y) {
        this.x = x;
        this.y = y;
    }
}

A slightly adjusted variant of the example from the documentation

public class PointAdapter extends TypeAdapter<Point> {
  public Point read(JsonReader reader) throws IOException {
    if (reader.peek() == JsonToken.NULL) {
      reader.nextNull();
      return null;
    }
    String xy = reader.nextString();
    String[] parts = xy.split(",");
    int x = Integer.parseInt(parts[0]);
    int y = Integer.parseInt(parts[1]);
    return new Point(x, y);
  }
  public void write(JsonWriter writer, Point point) throws IOException {
    if (point == null) {
      writer.nullValue();
      return;
    }
    String xy = point.x + "," + point.y;
    writer.value(xy);
  }
}

yields the desired result.

Solution 4

I tried an experiment with final fields - firstly setting them with initialisers, e.g.:

class Test {
    final int x = 1;
    private Test() {}
}

GSON would not deserialise this properly.

and then in the constructor...

class Test {
    final int x;
    private Test() {
    x = 1;
    }
}

This worked. Perhaps it's a Java compiler optimisation, where final primitive or String-type variables with initialisers are treated like conmpile-time constants, without an actual variable being created, whereas if the initialisation is done in the constructor, a variable is created?

Share:
14,280
Fabian Zeindl
Author by

Fabian Zeindl

Updated on June 04, 2022

Comments

  • Fabian Zeindl
    Fabian Zeindl almost 2 years

    i deserialize a widget-hierarchy using gson, but have problems deserializing final fields.

    Example:

    public final class Screen{
    
        @Expose
        private final List<WidgetDefinition> children       = null;
        @Expose
        private final String name           = null;
    }
    
    public final class AWidget implements WidgetDefinition {
        @Expose
        private final String name           = null;
    }
    

    i'm deserializing a Screen using a custom deserializer for WidgetDefinition, shown below. 'name' in Screen is set correctly, 'name' in AWidget stays null.

    final class Deserializer implements JsonDeserializer<WidgetDefinition> {
    
        public WidgetDefinition deserialize(final JsonElement json, final Type type,
                                            final JsonDeserializationContext context) {
    
            JsonObject jsonObject = json.getAsJsonObject();
    
            String typeName = jsonObject.get("type").getAsString();
            if (typeName.equals("awidget")) {
                return context.deserialize(json, AWidget.class);
            } else {
                return null;
            }
        }
    }
    

    Edit: I wonder if it has to do something with this:

    Gson 1.7 won’t serialize subclass fields in collection elements. 2.0 adds this extra information.

    (https://sites.google.com/site/gson/gson-roadmap)

  • Fabian Zeindl
    Fabian Zeindl over 12 years
    I found out more behaviour. If i have a field that is final and assigned to null (no constructor), gson will fill that field. If it's a primitive type or assigned to something else, like a string, gson won't touch it.
  • Programmer Bruce
    Programmer Bruce over 12 years
    Ack. Then Gson is even more inconsistent than I guessed it was.
  • Lii
    Lii about 2 years
    Why do you think that GSON deserialises value to final fields? That does not work for me. Do you have any link to documentation about this?
  • mkjeldsen
    mkjeldsen about 2 years
    No, I don't. It has been seven years and I have long since moved on from Gson. In any case, the point of my contribution was that Gson's default behaviour was worthlessly unpredictable and that manually coding deserialisers was preferable to my sanity.
  • Lii
    Lii about 2 years
    It seems like GSON works fine for final fields WITHOUT INITIALISERS. If there are initialisers it doesn't work. I agree with "worthlessly unpredictable".