using LightJson; public interface ExampleNocabInterface : JsonConvertible { } // NOTE: all JsonConvertable functions are virtual or abstract. public abstract class ExampleNocabAbstract : ExampleNocabInterface { private string abstractData; protected ExampleNocabAbstract(string abstractData) { this.abstractData = abstractData; } protected ExampleNocabAbstract(JsonObject jo) { this.abstractLoadJson(jo); } public virtual JsonObject toJson() { // Take data from this abstract class and convert to JsonObject JsonObject result = new JsonObject(); // Convert the rest of this class into a JO result["abstractData"] = abstractData; return result; } protected void abstractLoadJson(JsonObject jo) { // TODO: Validate the Json before loading // Take the data from the JO and load it into this abstract class this.abstractData = jo["abstractData"]; } public abstract void loadJson(JsonObject jo); } public class ExampleNocabChild : ExampleNocabAbstract { private int data; public ExampleNocabChild(int data) : base("random Data") { this.data = data; } public ExampleNocabChild(JsonObject jo) : base(jo["Base"].AsJsonObject) { /* Order of events * 1) An ExampleNocabInherit is created via 'new ExampleNocabInherit(jo);' * 2) Base class constructor is called * 2.1) Base constructor runs the abstractLoadJson() function * 3) This class loads its own data from the JO */ // The base class is already loaded, so only load data for this child class loadJsonThis(jo); } public override JsonObject toJson() { // Convert the base abstract class into a JO JsonObject result = new JsonObject(); result["Base"] = base.toJson(); // Convert the rest of this class into a JO result["data"] = data; return result; } private void loadJsonBase(JsonObject baseJO) { base.abstractLoadJson(baseJO); } private void loadJsonThis(JsonObject jo) { // Load data from the JO into this class this.data = jo["data"]; } public override void loadJson(JsonObject jo) { // TODO Validate the JO before loading // Load the base class first loadJsonBase(jo["Base"]); // Load the rest of the data from the JO into this class loadJsonThis(jo); } }
This tutorial builds upon the code discussed in the previous post found here Data Persistence 1: JSON Introduction. Please refer to the code dump at the bottom of that post to quickly get up to speed.
I’m using a library called LightJson created by MarconLopezC found here. To add this to your Unity project, first check the license, then click the "Clone or download" button and "Download ZIP". Unzip the package and copy the license, Readme, and cs files into your Unity project assets folder.
The previous post showed that converting a simple class into JSON is possible. However, before scaling up this process to many different types of objects or more complex classes, a standardized interface should enforced. I wanted to capture the behavior demonstrated previously in a single interface so as I move forward I can easily make any class “JSON-able”, or have the ability to be converted into JSON.
using LightJson; using System; public interface JsonConvertible { /** * A JsonConvertable object is an object that can convert itself into a JsonObject, * and ingest a well-formatted JsonObject and update itself to match. * * In other words, the JsonConvertable interface allows objects to save and load themselves. * * In general, the output of toJson() function should capture 100% of the state stored in * the object. The format of the outputted JsonObject from the toJson() function, should be * valid input for the loadJson(...) function. That is to say, this.loadJson(this.toJson()) * should produce no errors and result in no state changes (a no-op). */ // TODO: Separate out the load and save functionality? JsonObject toJson(); void loadJson(JsonObject jo); }
This JsonConvertible interface is simple and only captures two functions. One will convert the implementing object into JSON, and the other will accept and load the data from an inputted JsonObject into the implementing object.
Let’s use this new interface to update ExampleNocabChild class created previously.
public class ExampleNocabChild : JsonConvertible { public int data; public ExampleNocabChild(int data) { this.data = data; } public ExampleNocabChild(JsonObject jo) { // TODO Validate the JO before loading loadJson(jo); } public JsonObject toJson() { JsonObject result = new JsonObject(); // Convert/ add this class's data into a JO result["data"] = data; return result; } public void loadJson(JsonObject jo) { // TODO Validate the JO before loading // Load the data from the JO into this class this.data = jo["data"]; } }
Notice that the constructor now takes in a JsonObject and simply passes it to the new loadJson(...) function. This hints at a design problem associated with the interface approach. While on the positive side, the interface will help standardize and streamline future development, it also introduces some challenges. The loadJson(...) function kinda acts like a constructor itself, taking in data and producing a specific instance of ExampleNocabChild. Ideally, the interface could force children classes to create a specific constructor that takes in a JsonObject, but it can’t do that. This loadJson(...) function has to be public, so anything will be able to override an existing ExampleNocabChild object by inputting a new JsonObject. This is a major security problem, and opens the door to crazy weird bugs, but also presents an opportunity to implement an object pooling design pattern. Be very careful every time you call the loadJson(...) function. With great power comes great responsibility.
Additionally, in order to load Json into an object, the object must already be constructed. You can’t call a function on an object that hasn’t been instantiated yet. To work around this second design problem, I’ve updated the constructor so it takes in a JsonObject and it simply passes the it to the loadJson(...) function. The interface can’t enforce this constructor, but I strongly recommend it.
Let’s add in an interface and abstract class to the inheritance hierarchy. I want to enforce that all inheritors of this new interface are also a JsonConvertible, so my new ExampleNocabInterface will extend the JsonConvertible interface.
public interface ExampleNocabInterface : JsonConvertible { } public abstract class ExampleNocabAbstract : ExampleNocabInterface { } public class ExampleNocabChild : ExampleNocabAbstract { ... public ExampleNocabChild(int data) : base() { this.data = data; } public ExampleNocabChild(JsonObject jo) : base() { ... } ... }
Notice how the abstract class currently has a compile time error because it does not define the required functions from the interface. Let’s fix that by following the procedure outlined above for the ExampleNocabChild when it implemented the JsonConvertible. I’ll also throw in some simple data to make this example a little more realistic:
public abstract class ExampleNocabAbstract : ExampleNocabInterface { private string abstractData; protected ExampleNocabAbstract(string abstractData) { this.abstractData = abstractData; } protected ExampleNocabAbstract(JsonObject jo) { this.loadJson(jo); } public JsonObject toJson() { JsonObject result = new JsonObject(); // Convert this class into a JO result["abstractData"] = abstractData; return result; } public void loadJson(JsonObject jo) { // TODO: Validate the Json before loading // Take the data from the JO and load it into this abstract class this.abstractData = jo["abstractData"]; } }
Nothing new so far, just repeating the process from ExampleNocabChild. However, I introduced a bug. Take a moment and try to find it yourself first. Feel free to run the code yourself and create any kind of tester methods. To find it, you will need to consider how all the current code works together.
If you found it, good job. But for the rest of us let’s consider the following test example. Update the NocabTester MonoBehavior with the following code.
Running this code produces the following results in the Unity Console.
public class NocabTester : MonoBehaviour { private void Start() { ExampleNocabChild testChild = new ExampleNocabChild(1); ExampleNocabAbstract testAbstract = new ExampleNocabChild(2); ExampleNocabInterface testInterface = new ExampleNocabChild(3); Debug.Log(testChild.toJson()); Debug.Log(testAbstract.toJson()); Debug.Log(testInterface.toJson()); } }
Well there seems to be quite a few bugs in the current code. As you can see, casting the ExampleNocabChild as its parent abstract class or interface seems to override the child’s toJson() function. Moreover, the child class toJson() function loses the abstract class’s data, while the interface and abstract class loose the child class’s data.
This bug can be partly fixed by updating the child toJson() function so that it also considers the base abstract class. Here, I decided to leverage the already existing base.toJson() function and encapsulate it in the child’s JSON object. Additionally, any changes to the toJson() function should also be mirrored in the loadJson(...) function. In this way, the child class doesn’t have to know how the base class saves or loads the JSON data. The child class simply acts as a passthrough. Kinda like two way implementation hiding, neither the parent nor child know how the other saves or loads data.
public class ExampleNocabChild : ExampleNocabAbstract { ... public JsonObject toJson() { JsonObject result = new JsonObject(); // Convert the base abstract class into a JO result["Base"] = base.toJson(); // Convert/ add this class's data into a JO result["data"] = data; return result; } public void loadJson(JsonObject jo) { // TODO Validate the JO before loading // Load the base class first base.loadJson(jo["Base"]); // Load the data from the JO into this class this.data = jo["data"]; } }
I re-run the above test and see that this fix does allow for the child.toJson() function to capture the data from the parent abstract class.
My IDE gives me a little hint. Squiggly green lines warn me that ExampleNocabChild.toJson() function hides inherited member ExampleNocabAbstract.toJson(). It recommends that I use the “new” keyword, so lets try it.
public class ExampleNocabChild : ExampleNocabAbstract { ... public new JsonObject toJson() { ... } public new void loadJson(JsonObject jo) { ... } }
That did absolutely nothing. The first result for the search of “c# new keyword” produces a microsoft documentation page called Knowing When to Use Override and New Keywords. I’ll spare you the details but in short, the “override” keyword is needed to solve this problem.
I’ll update the code and try it.
public abstract class ExampleNocabAbstract : ExampleNocabInterface { public virtual JsonObject toJson() { ... } public virtual void loadJson(JsonObject jo) { ... } } public class ExampleNocabChild : ExampleNocabAbstract { public override JsonObject toJson() { ... } public override void loadJson(JsonObject jo) { ... } }
Success! But the bug hunting isn’t over. This fix comes with a cost. In C#, the virtual and override keywords act in unusual ways. Additional testing is needed, specifically for the loadJson(...) functionality. Consider the following code changes, debug logs and test example:
public class NocabTester : MonoBehaviour { private void Start() { ExampleNocabChild testA = new ExampleNocabChild(5); JsonObject jo = testA.toJson(); Debug.Log(jo.ToString()); ExampleNocabChild testB = new ExampleNocabChild(jo); Debug.Log(testB.toJson().ToString()); } }
public abstract class ExampleNocabAbstract : ExampleNocabInterface { ... protected ExampleNocabAbstract(JsonObject jo) { if (jo != null) { Debug.Log("Abstract constructor, Json is NOT null!"); } this.loadJson(jo); } ... public virtual void loadJson(JsonObject jo) { // TODO: Validate the Json before loading if (jo == null) { Debug.LogError("Abstract loadJson, Json IS null!"); } this.abstractData = jo["abstractData"]; } } public class ExampleNocabChild : ExampleNocabAbstract { ... public ExampleNocabChild(JsonObject jo) : base(jo["Base"].AsJsonObject) { // TODO Validate the JO before loading loadJson(jo); } }
In the ExampleNocabChild, I updated the JsonObject constructor to hand over the part of the JSON object that ExampleNocabAbstract is expecting. Some of you may see where the bug is now (and if so you are truly a god/dess and should ask your boss for a raise. It took me a long time to find), but for the rest of us consider the Tester Monobehavior. It converts a child object into JSON, then attempts to load that JSON into a different instance of ExampleNocabChild. The debug statements in ExampleNocabAbstract are simply to illustrate this very unique looking bug:
Somehow, the JSON object that was given to the abstract class went from being NOT null, to being null. It seems that the act of passing the JSON object between the ExampleNocabAbstract constructor and ExampleNocabAbstract.loadJson(...) function somehow corrupted it. The stack trace in the above console log shows exactly what the problem is. I’ll redo the code and input markings for each of the 5 lines in the stacktrace.
public class NocabTester : MonoBehaviour { private void Start() { ExampleNocabChild testA = new ExampleNocabChild(5); JsonObject jo = testA.toJson(); Debug.Log(jo.ToString()); /*1*/ ExampleNocabChild testB = new ExampleNocabChild(jo); Debug.Log(testB.toJson().ToString()); } } public abstract class ExampleNocabAbstract : ExampleNocabInterface { ... protected ExampleNocabAbstract(JsonObject jo) { if (jo != null) { Debug.Log("Abstract constructor, Json is NOT null!"); } /*3*/ this.loadJson(jo); } ... public virtual void loadJson(JsonObject jo) { // TODO: Validate the Json before loading if (jo == null) { Debug.LogError("Abstract loadJson, Json IS null!"); } /*5*/ this.abstractData = jo["abstractData"]; } } public class ExampleNocabChild : ExampleNocabAbstract { ... /*2*/ public ExampleNocabChild(JsonObject jo) : base(jo["Base"].AsJsonObject) { // TODO Validate the JO before loading loadJson(jo); } ... public override void loadJson(JsonObject jo) { // TODO Validate the JO before loading // Load the base class first /*4*/ base.loadJson(jo["Base"]); // Load the data from the JO into this class this.data = jo["data"]; } }
It turns out, the override keyword seems to change the expected behavior of the “this” keyword. At point 3 in the above code, the child loadJson(...) function is called, not the abstract parent’s loadJson(...) function. Then, point 4 in the code tries to extract the “Base” out of the JSON, which is a problem because point 2 already did the extraction. So, the JSON that arrives at point 5 will be null. There’s the bug, what’s the best way to fix it?
In theory at point 2 (in the child constructor), we could pass the entire JsonObject to the base constructor, which will then pass it back into the child loadJson(...) function, which then splits it up and passes the base back into the parent abstract class. But that seems... inelegant to me. I don’t want to trust that the parent abstract class will do the right thing in order to load the child class. I want each to be entirely responsible for themselves. I will admit that the solution that ended up using is not very elegant either and involves creating some internal loading helper functions. I justify this by telling myself that the functionality of loading data into a single class is different than the functionality of passing data into the base class, which is different from loading data into the entire class hierarchy. It’s contrived, but it works as shown below.
public abstract class ExampleNocabAbstract : ExampleNocabInterface { ... protected ExampleNocabAbstract(JsonObject jo) { this.abstractLoadJsonHelper(jo); } protected void abstractLoadJsonHelper(JsonObject jo) { // TODO: Validate the Json before loading // Take the data from the JO and load it into this abstract class this.abstractData = jo["abstractData"]; } public abstract void loadJson(JsonObject jo); } public class ExampleNocabChild : ExampleNocabAbstract { ... public ExampleNocabChild(JsonObject jo) : base(jo["Base"].AsJsonObject) { // TODO Validate the JO before loading loadJsonThisHelper(jo); } ... private void loadJsonThisHelper(JsonObject jo) { // TODO Validate the JO before loading /* Load data from the JO into this class */ this.data = jo["data"]; } public override void loadJson(JsonObject jo) { // TODO Validate the JO before loading // Load the base class first base.abstractLoadJsonHelper(jo["Base"]); // Load the data from the JO into this class this.loadJsonThisHelper(jo); } }
Dividing the functionality up in these helper functions allows for more persice control over what functions get called when. For example, the ExampleNocabChild constructor can pass the parent's part of the JSON data to be loaded, then only load data into itself. There is no need do run the normal interface loadJson(...) function because that will update the parent class again.
Running the test one final time produces the expected results. The child and parent abstract class can be converted into JSON, and that JSON can be loaded back into a new (or exiting) class to update the child and parent.
using LightJson; public interface ExampleNocabInterface : JsonConvertible { } // NOTE: all JsonConvertable functions are virtual or abstract. public abstract class ExampleNocabAbstract : ExampleNocabInterface { private string abstractData; protected ExampleNocabAbstract(string abstractData) { this.abstractData = abstractData; } protected ExampleNocabAbstract(JsonObject jo) { this.abstractLoadJson(jo); } public virtual JsonObject toJson() { // Take data from this abstract class and convert to JsonObject JsonObject result = new JsonObject(); // Convert the rest of this class into a JO result["abstractData"] = abstractData; return result; } protected void abstractLoadJson(JsonObject jo) { // TODO: Validate the Json before loading // Take the data from the JO and load it into this abstract class this.abstractData = jo["abstractData"]; } public abstract void loadJson(JsonObject jo); } public class ExampleNocabChild : ExampleNocabAbstract { private int data; public ExampleNocabChild(int data) : base("random Data") { this.data = data; } public ExampleNocabChild(JsonObject jo) : base(jo["Base"].AsJsonObject) { /* Order of events * 1) An ExampleNocabInherit is created via 'new ExampleNocabInherit(jo);' * 2) Base class constructor is called * 2.1) Base constructor runs the abstractLoadJson() function * 3) This class loads its own data from the JO */ // The base class is already loaded, so only load data for this child class loadJsonThis(jo); } public override JsonObject toJson() { // Convert the base abstract class into a JO JsonObject result = new JsonObject(); result["Base"] = base.toJson(); // Convert the rest of this class into a JO result["data"] = data; return result; } private void loadJsonBase(JsonObject baseJO) { base.abstractLoadJson(baseJO); } private void loadJsonThis(JsonObject jo) { // Load data from the JO into this class this.data = jo["data"]; } public override void loadJson(JsonObject jo) { // TODO Validate the JO before loading // Load the base class first loadJsonBase(jo["Base"]); // Load the rest of the data from the JO into this class loadJsonThis(jo); } }