Search Unity

"Using FromJson() when the type is not known ahead of time"

Discussion in 'Documentation' started by ersinakinci, Oct 5, 2016.

  1. ersinakinci

    ersinakinci

    Joined:
    Oct 5, 2016
    Posts:
    3
    From the manual chapter on JSON serialization (https://docs.unity3d.com/Manual/JSONSerialization.html):

    Using FromJson() when the type is not known ahead of time
    Deserialize the JSON into a class or struct that contains ‘common’ fields, and then use the values of those fields to work out what actual type you want. Then deserialize a second time into that type.

    My question is, what does that mean? What does "a class or struct that contains 'common' fields" mean? How are we supposed to work out the actual type? How do we deserialize a second time?
     
    IotBlue likes this.
  2. Graham-Dunnett

    Graham-Dunnett

    Administrator

    Joined:
    Jun 2, 2009
    Posts:
    4,287
    Hi Ersinakinci,

    I'll add a bug to request that this section of the page is improved. Can't say when the improvement will happen.

    I've not used FromJson() personally. But, I guess, if I was faced with FromJson() I'd start with wondering why the JSON is being sent. Where does it come from? Who's likely to send it? How often can I guess it'll arrive? If you could guess some details, make the class that the page recommends. See what arrives. A simple JSON file, like shown on the doc page would be trivial. Am sure much more, confusing, JSON files might be received by your app. The wikipedia page has some more complex JSON files, so might be helpful in working out what an unknown JSON file might be bringing.

    (Am sure some experts will get involved...)

    Regards,
    Graham
     
  3. Graham-Dunnett

    Graham-Dunnett

    Administrator

    Joined:
    Jun 2, 2009
    Posts:
    4,287
    Meant to add "a class or struct the contains common fields" is based on the names of things in the received JSON. If the JSON included a "annoying":"Graham", then the class includes a field called annoying. It's probably a string, of course, since it is attached to a string ("Graham"). Working out it's a string is the game. Deserialising twice means taking the JSON file and applying the values to your newly defined class/struct.
     
  4. ersinakinci

    ersinakinci

    Joined:
    Oct 5, 2016
    Posts:
    3
    Hi Graham, thanks for your response.

    To give you some context, the JSON isn't being fetched from over the web, I'm using a locally stored JSON file for some event data. The issue is that a few of my fields are polymorphic in the sense that the spec that I've created doesn't define the value type for those fields (e.g., "value" can equal "Hello," 6, or true). In other words, this behavior is intentional and can't be avoided in my use case.

    I'm not sure that I follow your elaboration, however.

    Your description is already the standard procedure as recommended in the API docs. The problem is that the property type has to be the same as the value type of the field. So if you had {"annoying": 6}, then klassInstance.annoying must be an int. My point is that there's no way to know that type at compile time if the field that you're deserializing has a polymorphic value type in the JSON document. You could make the property type object, and actually this is how I already solved my problem, but the documentation seems to be suggesting a different method.

    Yes, you're right that working out the type is the game, which is why I started this thread. The documentation section title "Using FromJson() when the type is not known ahead of time" implies some method for handling this situation, but it doesn't have one.

    I don't think that I follow. I'm pretty sure that's deserializing once. Unity loads the string into memory, lexes and parses it, then deserializes into objects. Where is the second time?
     
  5. Kalladystine

    Kalladystine

    Joined:
    Jan 12, 2015
    Posts:
    227
    The wording definitely needs improvement.
    I think this section applies to a different scenario than yours - it's not about when the field type inside the class is polymorphic (which is a special case by itself and needs very careful handling anyway or even a redesign if possible), but when you don't know to what class you need to deserialize.

    Example scenario:

    In an RPG you have your whole inventory serialized and saved somewhere.
    All classes used in the Inventory derive from ItemBase. ItemBase has only 2 fields - UniqueID and ItemType (enum).
    You deserialize each object as an ItemBase, parse Item.ItemType value into the enum and based on that parse the original JSON into the specialised class (Weapon, Armor, Potion etc.).

    Example code (tested, works):
    Long spoiler below.

    Btw - this is how I would imagine a decent JsonUtility "real life" example would look like. Feel free to use in the Docs :p

    Classes:

    ItemBase
    Code (CSharp):
    1. [Serializable]
    2. public class ItemBase
    3. {
    4.     public int UniqueID;
    5.     public ItemType ThisItemType;
    6.  
    7.     private static int globalUniqueItemID = 0;
    8.  
    9.     public ItemBase()
    10.     {
    11.         this.UniqueID = globalUniqueItemID;
    12.         globalUniqueItemID++;
    13.     }
    14.  
    15.     public enum ItemType
    16.     {
    17.         Other,
    18.         Weapon,
    19.         Armor,
    20.         Potion
    21.     }
    22. }
    MyWeapon
    Code (CSharp):
    1. [Serializable]
    2. public class MyWeaponClass : ItemBase
    3. {
    4.     public int MinDamage;
    5.     public int MaxDamage;
    6.  
    7.     public MyWeaponClass() : base()
    8.     {
    9.         this.ThisItemType = ItemType.Weapon;
    10.     }
    11.  
    12.     public MyWeaponClass(int minDmg, int maxDmg) : this()
    13.     {
    14.         this.MinDamage = minDmg;
    15.         this.MaxDamage = maxDmg;
    16.     }
    17.  
    18.     public override string ToString()
    19.     {
    20.         return String.Format("UniqueID: {0}, ItemType: {1}, MinDamage: {2}, MaxDamage: {3}.", UniqueID, ThisItemType, MinDamage, MaxDamage);
    21.     }
    22. }
    MyArmor
    Code (CSharp):
    1.  
    2. [Serializable]
    3. public class MyArmorClass : ItemBase
    4. {
    5.     public float ArmorValue;
    6.     public string OriginalOwnerName;
    7.  
    8.     public MyArmorClass() : base()
    9.     {
    10.         this.ThisItemType = ItemType.Armor;
    11.     }
    12.  
    13.     public MyArmorClass(float armorValue, string firstOwner) : this()
    14.     {
    15.         this.ArmorValue = armorValue;
    16.         this.OriginalOwnerName = firstOwner;
    17.     }
    18.  
    19.     public override string ToString()
    20.     {
    21.         return String.Format("UniqueID: {0}, ItemType: {1}, ArmorValue: {2}, FirstOwner: {3}.", UniqueID, ThisItemType, ArmorValue, OriginalOwnerName);
    22.     }
    23. }
    24.  
    Sample "application"
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. public class JsonExample : MonoBehaviour
    7. {
    8.     void Start()
    9.     {
    10.         // create 2 dummy items
    11.         MyArmorClass sampleArmor = new MyArmorClass(25.7f, "Awesome Joe");
    12.         MyWeaponClass sampleWeapon = new MyWeaponClass(10, 18);
    13.  
    14.         // make a dummy inventory
    15.         List<ItemBase> myInventory = new List<ItemBase>();
    16.         myInventory.Add(sampleArmor);
    17.         myInventory.Add(sampleWeapon);
    18.  
    19.         // print out the inventory
    20.         Debug.Log("==== printing original inventory ====");
    21.         foreach (var item in myInventory)
    22.         {
    23.             Debug.Log(item);
    24.         }
    25.  
    26.  
    27.         // serialize the whole inventory
    28.         List<string> myJsons = new List<string>();
    29.  
    30.         foreach (var item in myInventory)
    31.         {
    32.             myJsons.Add(JsonUtility.ToJson(item));
    33.         }
    34.  
    35.         // print out how the items look as json
    36.         Debug.Log("==== printing json serialized inventory ====");
    37.         foreach (string jsonString in myJsons)
    38.         {
    39.             Debug.Log(jsonString);
    40.         }
    41.  
    42.         // pretend that the jsons are stored somewhere
    43.         // for example in a file or server
    44.         // and later retrieved as a list of strings
    45.  
    46.         // deserialize back
    47.  
    48.         List<ItemBase> myDeserializedInventory = new List<ItemBase>();
    49.  
    50.         foreach (string jsonString in myJsons)
    51.         {
    52.             ItemBase unknownItem = JsonUtility.FromJson<ItemBase>(jsonString);
    53.             switch (unknownItem.ThisItemType)
    54.             {
    55.                 case ItemBase.ItemType.Weapon:
    56.                     MyWeaponClass tmpWeapon = JsonUtility.FromJson<MyWeaponClass>(jsonString);
    57.                     myDeserializedInventory.Add(tmpWeapon);
    58.                     break;
    59.                 case ItemBase.ItemType.Armor:
    60.                     MyArmorClass tmpArmor = JsonUtility.FromJson<MyArmorClass>(jsonString);
    61.                     myDeserializedInventory.Add(tmpArmor);
    62.                     break;
    63.                 default:
    64.                     Debug.LogError("Not implemented item type!");
    65.                     break;
    66.             }
    67.         }
    68.  
    69.         // print out the whole deserialized inventory
    70.         Debug.Log("==== printing deserialized inventory ====");
    71.         foreach (var item in myDeserializedInventory)
    72.         {
    73.             Debug.Log(item);
    74.         }
    75.         // count weapons
    76.         Debug.Log("Weapons: " + myDeserializedInventory.Where(x => x.ThisItemType == ItemBase.ItemType.Weapon).Count());
    77.         // count armor
    78.         Debug.Log("Armors: " + myDeserializedInventory.Where(x => x.ThisItemType == ItemBase.ItemType.Armor).Count());
    79.         // count total
    80.         Debug.Log("All items: " + myDeserializedInventory.Count());
    81.     }
    82. }
    83.  
    Console output:
    upload_2016-10-14_0-19-7.png

    Also it would be nice if there would be some explanation of this part "The object you pass in is fed to the standard Unity serializer for processing, so the same rules and limitations apply as they do in the Inspector; only fields are serialized, and types like Dictionary&lt;&gt;are not supported." so that it's obviously clear what is and what isn't serialized by it (it's probably somewhere in the docs about inspector, so a link maybe?).
     
    Last edited: Oct 13, 2016
    jwol likes this.
  6. dmaugis

    dmaugis

    Joined:
    Jun 22, 2017
    Posts:
    2
    I still do not understand the explanation...
     
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    From how I understand it, what they want to convey is that if you have these classes:

    Code (csharp):
    1. class Person {
    2.     public int age;
    3.     public string name;
    4. }
    5.  
    6. class House {
    7.     public float squareMeters;
    8.     public int numberOfDoors;
    9. }
    And you're receiving data that might be a Person or a House, you can make a container class that contains all of the possible data:

    Code (csharp):
    1. public class DataContainer {
    2.     public int age;
    3.     public string name;
    4.     public float squareMeters;
    5.     public int numberOfDoors
    6. }
    You can then deserialize the JSON as a DataContainer, check what the data is, and deserialize based on that:

    Code (csharp):
    1. DataContainer container = JsonUtility.FromJson<DataContainer>(json);
    2. if(container.name != "") {
    3.     Person p = JsonUtility.FromJson<Person>(json);
    4. }
    5. else {
    6.     House h = JsonUtility.FromJson<House>(json);
    7. }

    Now, I'd consider this a very brittle and somewhat absurd way of handling the issue. Serializing information about the type is much cleaner than trying to guess it. If you have many types, you'll also end up with a very large container.

    The only reason I can see for ever doing this is if you're not in control of the data being sent. In that case, you might have to use a method like this to coerce it into typed data.
     
    Vordyn likes this.
  8. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,659
    Actually, that's what I meant. Suppose you have JSON data like this:

    Code (csharp):
    1.  
    2. { "objectType": "person",
    3.   "name": "Superpig" }
    4.  
    5. { "objectType": "house",
    6.   "address": "123 Fake Street" }
    7.  
    The idea is that the 'objectType' field is 'common' between the types. So you'd do something like this:

    Code (csharp):
    1.  
    2. [Serializable] struct CommonObjectFields {
    3.    public string objectType;
    4. }
    5.  
    6. CommonObjectFields commonFields = JsonUtility.FromJson<CommonObjectFields>(json);
    7. switch (commonFields.objectType)
    8. {
    9.     case "person":
    10.         DoSomethingWithPerson(JsonUtility.FromJson<Person>(json));
    11.         break;
    12.     case "house":
    13.         DoSomethingWithHouse(JsonUtility.FromJson<House>(json));
    14.         break;
    15. }
    16.  
    You deserialise once into a structure that has enough fields to work out what type to use; then deserialise again into that type.
     
  9. ysftulek

    ysftulek

    Joined:
    Aug 14, 2015
    Posts:
    46
    Sorry for necroposting but I really want to learn about this. Is this double serialization even useful? Except for the point Baste made:

    The only reason I can see for ever doing this is if you're not in control of the data being sent. In that case, you might have to use a method like this to coerce it into typed data.