4

I have been working on a project which needs to save and load data through a JSON file. This JSON file includes various lists of other objects. When I go ahead and deserialize the file, however, this happens:

System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported.

The code that takes care of deserializing is as follows:

        public void LoadFromJson() {

        int userCount = File.ReadAllLines(folder + "/users").Length;
        int shopCount = File.ReadAllLines(folder + "/shops").Length;
        using (FileStream fileUsers = File.Open(folder + "/users", FileMode.Open, FileAccess.Read)) {
            StreamReader srUser = new StreamReader(fileUsers);
            for(int i=0; i<userCount; i++){
                ListOfUsers.Add(JsonSerializer.Deserialize<User>(srUser.ReadLine()));
            }
            srUser.Close();
            fileUsers.Close();
        }  

        using (FileStream fileShops = File.Open(folder + "/shops", FileMode.Open, FileAccess.Read)){
            StreamReader srShops = new StreamReader(fileShops);
            for(int i=0; i<shopCount; i++){
                ListOfShops.Add(JsonSerializer.Deserialize<Shop>(srShops.ReadLine()));
            }
            srShops.Close();
            fileShops.Close();
        }       

    }

Classes I'm trying to deserialize

    public abstract class Shop{

    public List<Sellable> ShopList { get; set; }
    public int TotalValue { get; set; }
    public string shopName { get; set; }

    public Shop(List<Sellable> list, string shopname){
        ShopList = list;
        shopName = shopname;
    }

    public abstract bool AddToShop(Sellable item);
    public abstract bool RemoveFromShop(string item);
    public abstract int GetValue(string name);
    public abstract string PrintShop();

}

    public abstract class Furniture : Sellable{
    public int Space { get; set; }
    public Conditions Condition { get; set; }
    public Materials Material { get; set; }

    [JsonConstructorAttribute]
    public Furniture(int val, int spc, string nm, Conditions condition, Materials material) : base (val, nm){
        Space = spc;
        Condition = condition;
        Material = material;
    }

}

    public abstract class Sellable{
    public int Value { get; set; }
    public string Name { get; set; }

    [JsonConstructorAttribute]
    public Sellable(int val, string name){
        Value = val;
        Name = name;
    }

JsonConverter

    public class SellableConverter : JsonConverter<Sellable>{ 

    public enum Type{
        Sellable,
        Furniture,
        Wearable,

    }

    public override Sellable Read(ref Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException();

        if (!reader.Read()
                || reader.TokenType != JsonTokenType.PropertyName
                || reader.GetString() != "Type") throw new JsonException();

        if (!reader.Read() || reader.TokenType != JsonTokenType.Number) throw new JsonException();

        Sellable baseClass;
        Type typeDiscriminator = (Type)reader.GetInt32();
        switch (typeDiscriminator)
        {
            case Type.Furniture:
                if (!reader.Read() || reader.GetString() != "TypeValue") throw new JsonException();
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) throw new JsonException();
                baseClass = (Furniture.Furniture)JsonSerializer.Deserialize(ref reader, typeof(Furniture.Furniture), options);
                break;
            case Type.Wearable:
                if (!reader.Read() || reader.GetString() != "TypeValue") throw new JsonException();
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) throw new JsonException();
                baseClass = (Wearable)JsonSerializer.Deserialize(ref reader, typeof(Wearable), options);
                break;
            case Type.Sellable:
                if (!reader.Read() || reader.GetString() != "TypeValue") throw new JsonException();
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) throw new JsonException();
                baseClass = (Sellable)JsonSerializer.Deserialize(ref reader, typeof(Sellable));
                break;
            default:
                throw new NotSupportedException();
        }

        if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject) throw new JsonException();

        return baseClass;
    }

    public override void Write(Utf8JsonWriter writer, Sellable value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        if (value is Furniture.Furniture derivedA)
        {
            writer.WriteNumber("TypeDiscriminator", (int)Type.Furniture);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedA, options);
        }
        else if (value is Wearable derivedB)
        {
            writer.WriteNumber("TypeDiscriminator", (int)Type.Wearable);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedB, options);
        }
        else if (value is Sellable baseClass)
        {
            writer.WriteNumber("TypeDiscriminator", (int)Type.Sellable);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, baseClass);
        }
        else throw new NotSupportedException();

        writer.WriteEndObject();
    }
}

SaveToJson method:

        public void SaveToJson(){

        FileStream fileUsers;
        FileStream fileShops;

        if(!(File.Exists(folder + "/users"))) fileUsers = File.Create(folder + "/users");
        else fileUsers = File.OpenWrite(folder + "/users");
        if(!(File.Exists(folder + "/shops"))) fileShops = File.Create(folder + "/shops");
        else fileShops = File.OpenWrite(folder + "/shops");

        StreamWriter srUser = new StreamWriter(fileUsers);
        StreamWriter srShop = new StreamWriter(fileShops);

        var serializeOptions = new JsonSerializerOptions();
        serializeOptions.Converters.Add(new SellableConverter());

        for(int i=0; i<ListOfUsers.Count; i++){
            srUser.WriteLine(JsonSerializer.Serialize<User>(ListOfUsers[i]), serializeOptions);
            Console.WriteLine("Debug: " + "\n" + "Object: " + ListOfUsers[i] + "\n" + "Json: " + JsonSerializer.Serialize<User>(ListOfUsers[i]));
        }
        for(int i=0; i<ListOfShops.Count; i++){
            srShop.WriteLine(JsonSerializer.Serialize<Shop>(ListOfShops[i]), serializeOptions);
            Console.WriteLine("Debug: " + "\n" + "Object: " + ListOfShops[i] + "\n" + "Json: " + JsonSerializer.Serialize<Shop>(ListOfShops[i]));
        }

        srUser.Close();
        fileUsers.Close();
        srShop.Close();
        fileShops.Close();

    }

How can I fix it? Thanks in advance! :)

EDIT: I added the classes I'm trying to deserialize. I apologize if I'm not giving enough detail or I make dumb mistakes, I am still a student and trying to mess with most of these things for the first time

xtremethegamer
  • 59
  • 1
  • 2
  • 6
  • The problem with your question is that, you post the error but don't post the relevant code. – aybe Jan 30 '21 at 02:25
  • Please share a [mcve], specifically the type `Shop` you are trying to deserialize. But see [How to use immutable types and non-public accessors with System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-5-0) for how to deal with types without a public parameterless constructor in general. – dbc Jan 30 '21 at 02:45
  • I hope I added enough info. I'm sorry if I made mistakes in specifying the code needed to understand the issue, but I'm not very knowledgeable on the matter yet! I apologize again – xtremethegamer Jan 30 '21 at 03:03

3 Answers3

7

Your exception says that you can't deserialize classes that don't have a default (parameterless) constructor.

The class you are deserializing, or one of the classes included as a property of that class, has a constructor that takes arguments, and doesn't also have a default constructor.

The deserializer isn't able to create an instance of that class, since it doesn't have the parameters to pass to that constructor.

Without seeing the definition of the classes you are trying to deserialize, I'm not able to help any further.

EDIT: It looks like Newtonsoft.Json was good enough to give some attributes, which you are ALMOST using correctly, to get around this problem. The JsonConstructorAttribute will match up properties from the serialized string being deserialized with constructor parameters, provided the names match (ignoring case).

[JsonConstructor]
public Sellable(int value, string name){
    Value = value;
    Name = name;
}

Thanks to Brian Rogers' answer for the clue I needed to find the relevant documentation!

Andy
  • 648
  • 8
  • 21
  • I added the classes I'm trying to serialize. I hope it's enough, as stated in the edit, I'm a student and I am not very knowledgeable of the matter (JSON and serialization/deserialization), and I rarely ever used forums to ask for help. Sorry for the inconvenience! – xtremethegamer Jan 30 '21 at 03:04
  • The issue is that all 3 of your classes only define constructors that take parameters and so the serializer can't create instances of those classes. If you add another constructor to each of your classes that take no parameters, your serializer will be able to deserialize those classes. We were all new once; you'll get there! :-) – Andy Jan 30 '21 at 03:09
  • I see, but wouldn't using a parameterless constructor fail to correctly insert values into the variables? – xtremethegamer Jan 30 '21 at 03:12
  • I've never used this before, but there might be a way based on this other SO answer: https://stackoverflow.com/a/23017892/1429354. Basically, add `[JsonConstructor]` immediately above the constructors and it will try to match property names from the JSON to constructor parameter names. I learned something new today! :-) – Andy Jan 30 '21 at 03:22
  • I see you're already using `[JsonConstructor]` (the word Attribute is optional). You need to make sure the parameter names match the property names. So `list` needs to become `shopList`, `val` becomes `value`, etc. – Andy Jan 30 '21 at 03:25
  • Great catch by Bruno! You can't deserialize an interface or abstract class, so you need to change `Shop` to no longer be abstract. – Andy Jan 30 '21 at 03:38
  • I see! However, what if I must have an abstraction? Isn't there a way to instantiate a child object in place? For example, Furniture, being a class that extends Sellable. So it would add to the List an object of type Furniture – xtremethegamer Jan 30 '21 at 14:49
0

The error says exactly what is wrong and how to fix it.

The target classes of deserializations (User and Shop) needs:

  • a parameterless constructor (IMHO the easiest, and most straightforward way);
  • a singular parameterized constructor; OR
  • a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported.

Alternatively you could deserialize to an dynamic or object and map the properties (See Deserialize JSON into C# dynamic object? for a small sample)

Also, and this REALLY bugs me, why are you reading the file and then reading each line? You should do something like

using(var fileUsers = File.Open(folder + "/users", FileMode.Open, FileAccess.Read))
using(var srUser = new StreamReader(fileUsers))
{
    string line;
    while((line = srUser.ReadLine()) != null)
    {
        ListOfUsers.Add(JsonSerializer.Deserialize<User>(line));
    }
}
Bruno Canettieri
  • 501
  • 2
  • 11
  • The reason mainly is that I'm still learning most of this stuff. I obviously look around but I'm a student, and whilst i know the concepts and some syntax, I still need to learn a lot to optimize the code. As per the parameterless constructor, wouldn't it fail to then add the various values to the variables? – xtremethegamer Jan 30 '21 at 02:57
  • That is alright. The deserializer does exactly that. It tries to automatically match the properties of the class with the names provided on the json, but first it must create an instance of the class (using a constructor). I see you updated your question, but the target class can't be abstract (meaning you can't instantiate it). – Bruno Canettieri Jan 30 '21 at 03:23
  • However, what if I must have an abstraction? Isn't there a way to instantiate a child object in place? For example, Furniture, being a class that extends Sellable. So it would add to the List an object of type Furniture – xtremethegamer Jan 30 '21 at 14:49
  • Yeah, you can deserialize a child object and add it to a collection of a parent, no problem there. But the class being deserialized can't be abstract, since you can't instantiate one. In you example, Furniture is also abstract. – Bruno Canettieri Jan 30 '21 at 17:07
  • I turned Furniture into a regular class, the issue is "Sellable". Since shop has a List I get the same error now with Sellable, since it's an abstract class. I don't know how to solve the issue – xtremethegamer Jan 30 '21 at 17:22
  • Oh, I get it now. Did not think this through. When you are deserializing to the Shop class you have a List and the deserializer don't know which implmentation it should use on each ShopList item (should it be a Furniture? Other? It simply don't have a way to know). Like in https://dotnetfiddle.net/fF4sRU In this case you will have to tell it how to differentiate it, you can get a rough example in https://attach2process.wordpress.com/2017/02/19/deserializing-a-json-to-a-list-of-abstract-types-or-interfaces-with-newtonsoft-json-net-converter/ – Bruno Canettieri Jan 30 '21 at 21:43
  • I tried looking into it with the System.Text.Json I'm using. I managed to implement it fine, it seemed, but when I now run the serialization process (passing the serializeOptions parameter) I get a new exception System.FormatException. I put the new code in the main post – xtremethegamer Jan 31 '21 at 03:36
  • Your current code is too different from the original. That makes it difficult to help you. But again, you should start by making it step by step. Like first serialize/deserialize just a Furniture, than a List than a List than a Shop(...). Also, strips this classes at first (just let each one have one property with a string value) than build on it until you find the error. Than you will know more clearly what prompt your error (and then you can ask a more focused question). – Bruno Canettieri Jan 31 '21 at 14:33
  • I managed to fix the problem by decomposing the "Shop" object, serializing the individual components, and recreating it upon reading. Thank you so much for your help! :) – xtremethegamer Jan 31 '21 at 23:46
0

I got the a similar runtime error using Blazor WASM (Blazor WebAssembly) and System.Text.Json:

fail: MyProject.Client.Shared.Error[0] Error:ProcessError - Type: System.NotSupportedException Message: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyProject.Shared.Models.DTO.MyDto'. Path: $[0].myDtos[0] | LineNumber: 0 | BytePositionInLine: 406. Exception: System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyProject.Shared.Models.DTO.MyDto'. Path: $[0].myDtos[0] | LineNumber: 0 | BytePositionInLine: 406. ---> System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyProject.Shared.Models.DTO.MyDto'.

Using you model the easiest fix in my world is simply adding a parameterless constructor:

public Sellable(){

}

If you need default values use constructor chaining which also works:

public Sellable() : this(0, "")
{
}

https://stackoverflow.com/a/1814965/3850405

Ogglas
  • 62,132
  • 37
  • 328
  • 418