Search code examples
c#jsonunity-game-enginejson.netdeserialization

Unity/JSON.NET Deserializing Abstract class that inherits from MonoBehaviour


I am trying to deserialize an abstract class that inherits from MonoBehaviour, I am able to serialize perfectly fine but upon deserialization I get these errors:

You are trying to create a MonoBehaviour using the 'new' keyword. This is not allowed. MonoBehaviours can only be added using AddComponent(). Alternatively, your script can inherit from ScriptableObject or no base class at all

and

ArgumentException: The value "null" is not of type "SerializableMeasurement" and cannot be used in this generic collection. Parameter name: value

All measurement types are derived from the abstract Measurement class, then upon serialization converted to their respected derived abstract Serializable Measurement classes which are omitted for now for brevity, they simply add member variables and operations which are unrelated to the issue.

The Measurement class is a type of node in my tree data structure, the serialization and deserialization for the other nodes work as intended.

If you'd like to look at the derived measurement classes and nodes, see the following scripts on my repo:

Other Nodes and Serializers:

Derived Measurements:

Removing the inheritance from MonoBehaviour isn't an option with how the project works.

Example JSON:

{
  "name": "Untitled v2",
  "subClassId": 1,
  "subFields": [
    {
      "name": "Testing Measurements​",
      "subClassId": 2,
      "measurements": [
        {
          "measureType": 0,
          "length": 0.231667489,
          "vertices": [
            {
              "x": -0.323850036,
              "y": -0.338313431,
              "z": 0.05710357
            },
            {
              "x": -0.323850065,
              "y": -0.228205621,
              "z": 0.07805062
            },
            {
              "x": -0.323850065,
              "y": -0.2833087,
              "z": 0.1503753
            },
            {
              "x": -0.323850036,
              "y": -0.229445323,
              "z": 0.184519753
            },
            {
              "x": -0.323850036,
              "y": -0.3353998,
              "z": 0.239965171
            }
          ]
        },
        {
          "pointCenter": {
            "x": -0.32385,
            "y": -0.2775318,
            "z": 0.356699169
          },
          "surfaceNormal": {
            "x": -1.0,
            "y": -5.13556273E-08,
            "z": 1.39479937E-07
          },
          "pointRadius": {
            "x": -0.32385,
            "y": -0.238266677,
            "z": 0.432952285
          },
          "measureType": 1,
          "area": 0.02311046
        },
        {
          "point1": {
            "x": -0.323850065,
            "y": -0.346135437,
            "z": 0.4835336
          },
          "point2": {
            "x": -0.323850036,
            "y": -0.3418154,
            "z": 0.623248458
          },
          "point3": {
            "x": -0.323850065,
            "y": -0.228444323,
            "z": 0.620691
          },
          "surfaceNormal": {
            "x": -1.0,
            "y": -5.13556273E-08,
            "z": 1.39479937E-07
          },
          "measureType": 2,
          "area": 0.0158506744
        },
        {
          "measureType": 3,
          "area": 0.0491224,
          "vertices": [
            {
              "x": -0.32385,
              "y": -0.157076448,
              "z": 0.205962881
            },
            {
              "x": -0.32385,
              "y": -0.0197166521,
              "z": 0.174950376
            },
            {
              "x": -0.32385,
              "y": -0.0266412925,
              "z": 0.404189438
            },
            {
              "x": -0.32385,
              "y": -0.0752793,
              "z": 0.418026716
            },
            {
              "x": -0.32385,
              "y": -0.126498729,
              "z": 0.441788167
            },
            {
              "x": -0.323850036,
              "y": -0.158409789,
              "z": 0.3402038
            },
            {
              "x": -0.32385,
              "y": -0.17995587,
              "z": 0.2563426
            }
          ]
        }
      ]
    }
  ]
}

Abstract Measurement Class:

public abstract class Measurement : MonoBehaviour
{
    public Vector3 previewPoint;

    public bool hidePreviewPoint;
    public float quantity;
    public string unit;
    public abstract void AddPointWithNormal(Vector3 point, Vector3 normal);

    protected abstract float CalculateQuantity();
    public void SetQuantity()
    {
        quantity = CalculateQuantity();
    }

    public int measureType;
    public Measurement(int _MeasureType, string _unit)
    {
        measureType = _MeasureType;
        unit = _unit;
    }

    public abstract SerializableMeasurement GetSerializable();
    public abstract void ConstructFromSerializable(SerializableMeasurement sm);
}

Abstract Serializable Measurement Class:

[JsonConverter(typeof(MeasurementConverter))]
public abstract class SerializableMeasurement
{
    [JsonProperty(Order = 0)]
    public int measureType { get; set; }
    public SerializableMeasurement(int measureType)
    {
        this.measureType = measureType;
    }
    public abstract string getMeasurementString();
}

Measurement Converter:

public class MeasurementConcreteClassConverter : DefaultContractResolver
{
    protected override JsonConverter ResolveContractConverter(Type objectType)
    {
        if (typeof(SerializableMeasurement).IsAssignableFrom(objectType) && !objectType.IsAbstract)
            return null; // pretend TableSortRuleConvert is not specified (thus avoiding a stack overflow)
        return base.ResolveContractConverter(objectType);
    }
}
public class MeasurementConverter : JsonConverter
{
    static JsonSerializerSettings SpecifiedSubclassConversion = new JsonSerializerSettings() { ContractResolver = new MeasurementConcreteClassConverter() };

    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(SerializableMeasurement));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jsonObject = JObject.Load(reader);

        // Handles deserialization for each type of measurement

        return jsonObject["measureType"].Value<int>() switch
        {
            0 => JsonConvert.DeserializeObject<Measure_LineSeg>(jsonObject.ToString(), SpecifiedSubclassConversion),
            1 => JsonConvert.DeserializeObject<Measure_Circular>(jsonObject.ToString(), SpecifiedSubclassConversion),
            2 => JsonConvert.DeserializeObject<Measure_Rectangular>(jsonObject.ToString(), SpecifiedSubclassConversion),
            3 => JsonConvert.DeserializeObject<Measure_PolySurface>(jsonObject.ToString(), SpecifiedSubclassConversion),
            _ => throw new Exception($"Unknown measurement type {jsonObject["subClassId"].Value<int>()}"),
        };
    }
}

Solution

  • You are trying to directly serialize to Measure_LineSeg etc which are MonoBehaviour components. As the error/warning tells you it makes no sense to create new instances of a MonoBehaviour without them being attached to any GameObject.

    Removing the inheritance from MonoBehaviour isn't an option with how the project works.

    If you create those instances via the constructor it is a very obvious hint that those shouldn't be MonoBehaviour in the first place! In your case the constructor though seems to rather be a way of trying to solve the deserialization issue.

    A MonoBehaviour shouldn't have any constructor at all as they are created and maintained by the underlying native UnityEngine and the c# instance of it is just an API layer on top of it!

    Rather have a method like e.g.

    public void Initialize(SerializableMeasurement data)
    {
        ...
    }
    

    Then you should rather deserialize into SerializeMeasure_LineSeg etc

    return jsonObject["measureType"].Value<int>() switch
        {
            0 => JsonConvert.DeserializeObject<SerializeMeasure_LineSeg>(jsonObject.ToString(), SpecifiedSubclassConversion),
            1 => JsonConvert.DeserializeObject<SerializeMeasure_Circular>(jsonObject.ToString(), SpecifiedSubclassConversion),
            2 => JsonConvert.DeserializeObject<SerializeMeasure_Rectangular>(jsonObject.ToString(), SpecifiedSubclassConversion),
            3 => JsonConvert.DeserializeObject<SerializeMeasure_PolySurface>(jsonObject.ToString(), SpecifiedSubclassConversion),
            _ => throw new Exception($"Unknown measurement type {jsonObject["subClassId"].Value<int>()}"),
        };
    

    which only holds the information and then have a loop running over all of those that spawns the according GameObjects with according components from prefabs into the scene and initializes them with the given data.

    Btw for the type instead of a nothing-saying int I would probably rather have an

    public enum MeasureType : byte
    {
        LineSeg,
        Circular,
        Rectangular,
        PolySurface
    }