2

First of all, I'd like to say this is for a university project.

I have 3 classes. Order abstract class and Delivery and DineIn classes which inherits from Order.

I am using Gson to serialize/deserialize the child classes but I have run into a bit of a problem. The Order class has a field orderType which gson uses to determine type of order it is, DineIn or Delivery.

Serialization is working just fine. The problem is that whenever I try to deserialize, the type field value is not read and is always set as null even though it is present in the JSON file. This happens when there are a lot of fields in Order because when I tried testing this program on a smaller scale with the Order class just having 2 fields (orderType and orderNo) everything worked just fine. I don't what I am doing wrong. I have tried searching on this site and am almost always coming across suggestions to make custom type adapters and serializers but we haven't studied about them in university and I don't want to use them (the instructor deducts marks for using anything he hasn't taught, I almost failed a course I took from him last time because I used things he hadn't taught. He doesn't seem to have a problem with third-party libraries though).

The code:

public class Main {
    public static final List<Order> ordersList = read();
    public static void main(String[] args) {
        System.out.println(ordersList.get(0).getOrderType());
        System.out.println(ordersList.get(0) instanceof DineIn ? "DineIn": "Delivery");
    }

    private static List<Order> read(){
        List<Order> ordersList = new ArrayList<>();
        Type type = new TypeToken<ArrayList<Order>>() {
        }.getType();


        RuntimeTypeAdapterFactory<Order> adapter = RuntimeTypeAdapterFactory.of(Order.class, "orderType")
                .registerSubtype(DineIn.class)
                .registerSubtype(Delivery.class);

        Gson gson = new GsonBuilder().registerTypeAdapterFactory(adapter).create();
        JsonReader ordersJsonReader;
        try {
            ordersJsonReader = new JsonReader(new FileReader("orders.json"));
            List<Order> tempOrdersList = gson.fromJson(ordersJsonReader, type);
            if (tempOrdersList != null) ordersList = tempOrdersList;
            ordersJsonReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ordersList;
    }
}

abstract class Order {
    private final int orderNumber;
    private final String date, customerName;
    private final int discountRate;
    private final String paymentMethod;
    private String orderStatus;
    private int grossTotal = 0;
    private double netTotal = 0;
    private int totalItems = 0;
    protected final String orderType;

    public abstract String getOrderType();
    public abstract double getExtraCharges();

    public Order(int orderNumber, String date, String customerName, int discountRate, String paymentMethod, String orderStatus, int grossTotal, double netTotal, int totalItems, String orderType) {
        this.orderNumber = orderNumber;
        this.date = date;
        this.customerName = customerName;
        this.discountRate = discountRate;
        this.paymentMethod = paymentMethod;
        this.orderStatus = orderStatus;
        this.grossTotal = grossTotal;
        this.netTotal = netTotal;
        this.totalItems = totalItems;
        this.orderType = orderType;
    }
}

class DineIn extends Order {
    private double serviceCharges = 150;

    public DineIn(int orderNumber, String date, String customerName, int discountRate, String paymentMethod, String orderStatus, int grossTotal, double netTotal, int totalItems) {
        super(orderNumber, date, customerName, discountRate, paymentMethod, orderStatus, grossTotal, netTotal, totalItems, "DineIn");
    }

    @Override
    public String getOrderType() {
        return orderType;
    }

    @Override
    public double getExtraCharges() {
        return serviceCharges;
    }
}

class Delivery extends Order {
    private double deliveryCharges = 100;

    public Delivery(int orderNumber, String date, String customerName, int discountRate, String paymentMethod, String orderStatus, int grossTotal, double netTotal, int totalItems) {
        super(orderNumber, date, customerName, discountRate, paymentMethod, orderStatus, grossTotal, netTotal, totalItems, "Delivery");
    }

    @Override
    public String getOrderType() {
        return orderType;
    }

    @Override
    public double getExtraCharges() {
        return deliveryCharges;
    }
}

The JSON:

[
  {
    "serviceCharges": 150.0,
    "orderNumber": 1,
    "date": "12/12/2021",
    "customerName": "Ali",
    "discountRate": 15,
    "paymentMethod": "Card",
    "orderStatus": "Preparing",
    "grossTotal": 5000,
    "netTotal": 4500.0,
    "totalItems": 14,
    "orderType": "DineIn"
  }
]
nitesh_
  • 31
  • 4
  • 1
    The `orderType` field is excluded by default when the DTO is being deserialized (at least prior this change: https://github.com/google/gson/commit/c1e7e2d2808b042cbe47ca31869ee6ccc62c5417#diff-d541ed0000ca9f491307d6f826a61c00992cd64c13136d6cbd3057f7f272a381 ). Make sure you have a "maintain"-aware copy of `RuntimeTypeAdapterFactory` and set the `maintain` flag to `true` so that the field could be restored. – terrorrussia-keeps-killing Jan 05 '22 at 13:00
  • 2
    By the way, Gson does not use constructors at all for simple objects like that when there are no default constructors (yes, there are ways of doing it in a dirty way) -- so Gson does not invoke the `DineIn` constructor (you can even add `throw new AssertionError("boom!");`) to the constructor body, and you'll still see that the `DineIn` is successfully deserialized). – terrorrussia-keeps-killing Jan 05 '22 at 13:02
  • 1
    And the last, from the design perspective, you don't need the `orderType` field at all: it duplicates the `getOrderType` method semantically so you can even remove that field and make the method return type discriminator strings directly, not using a field (and even not using a constructor that may be not invoked by Gson as you could see). I'd remove both field and method: `getClass` is fine enough doing the same. – terrorrussia-keeps-killing Jan 05 '22 at 13:05
  • So, what should I do with the adapter? It would need some info to deserialize correctly right? – nitesh_ Jan 05 '22 at 14:42
  • 1
    Please re-read my comments #1 (use the `maintain` flag if you really want to retain the field) and #3 (for the design perspective since you need neither the discriminator field nor the discriminator method preferring `getClass` if possible). – terrorrussia-keeps-killing Jan 05 '22 at 14:47
  • Sorry, I should have clarified. Since what is being written into the file is DineIn and Delivery and it is being read into a List and I don't need the discriminator what should I do with the runtimeAdapterFactory? Leave it as it is with the maintain flag or is there an alternative where it can be removed and the program works as intended. – nitesh_ Jan 05 '22 at 15:05
  • 1
    Again, you need the field for JSON representation **only** (to retain the minimal type information for your deserializers and other JSON consumers where that information may matter) and don't need it for "real" Java objects that already have **known** type once they are `new`-ed per se. Having that said, remove the field from the DTO classes, but let your `RuntimeTypeAdapterFactory` still use the `objectType` so that the result JSON could then be restored to its original object. – terrorrussia-keeps-killing Jan 05 '22 at 15:19
  • 1
    Check this out: `public abstract static class Base {}`, `public static final class A extends Base {}`, and `public static final class B extends Base {}`. Let `Gson` instance to be instantiated like this: `new GsonBuilder().registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(Base.class, "__type").registerSubtype(A.class, "__A__").registerSubtype(B.class, "__B__")).create()`. – terrorrussia-keeps-killing Jan 05 '22 at 15:20
  • 1
    Then test it: `final Base[] before = { new A(), new B() };`, `final String json = gson.toJson(before, Base[].class);`, `System.out.println(json);`, `final Base[] after = gson.fromJson(json, Base[].class);`, and `for ( final Base base : after ) {System.out.println(base.getClass());}`. For the first print it will produce `[{"__type":"__A__"},{"__type":"__B__"}]` containing ALL necessary type info. For the second print it will produce something like `Main$A` and `Main$B` (just because these two classes are nested). – terrorrussia-keeps-killing Jan 05 '22 at 15:22
  • Thank you very much! I understood it perfectly. Thanks for taking out the time to reply and help me understand! I really appreciate it! – nitesh_ Jan 05 '22 at 16:59

1 Answers1

1

In your code you have a hierarchy where DineIn and Delivery extend from Order. The way the orderType field is set is through an explicit String argument in the super() constructor.

However, Gson does not use the constructor to instantiate the objects. It uses a special no-argument constructor and populates the values via reflection: https://stackoverflow.com/a/40442037/9698467

In this specific case the problem comes from the RuntimeTypeAdapterFactory, which removes the orderType field from the JSON that it reads. The source code here confirms that: https://github.com/google/gson/blob/86d88c32cf6a6b7a6e0bbc855d76e4ccf6f120bb/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java#L202

As @fluffy suggested newer versions of the library include the maintain flag, which should allow for the field to be preserved: https://github.com/google/gson/blob/c1e7e2d2808b042cbe47ca31869ee6ccc62c5417/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java#L214

Stefan Zhelyazkov
  • 2,599
  • 4
  • 16
  • 41