1

I had a class that I was regularly serializing and deserializing. One of its fields used to be a string to represent a road, but has since been changed to its own class.

class Foo
{
    public string RoadRef;
}

Has now been changed to:

class Foo
{
    public Road RoadRef;
}

class Road
{
    public Road(string val)
    {
        Lane = val[0];
        Number = int.Parse(val.Substring(1));
    }

    public char Lane;
    public int Number = 1;
}

Now I'm getting errors when I try to deserialize from strings that were serialized with the old version of the class. I have a lot of old serialized files that I don't want to go back and reserialize to the new format, especially since changes like this will likely happen again.

I should be able to specify a custom method to Deserialize (and Serialize) for JsonConvert.Deserialize right? Is there a better approach to deal with this problem?

The closest I've got so far is adding a [JsonConstructor] attribute above the constructor as suggested here, but it didn't help the deserializing crashes.

The original JSON looks like this: {"RoadRef":"L1"} It throws this exception:

Newtonsoft.Json.JsonSerializationException: 'Error converting value "L1" to type 'Road'. Path 'RoadRef', line 1, position 15.'

ArgumentException: Could not cast or convert from System.String to Vis_Manager.Road.

Moffen
  • 1,817
  • 1
  • 14
  • 34
  • What does your Json look like? – traveler3468 Apr 16 '19 at 22:55
  • 1
    I think you might need a [custom JsonConverter](https://www.newtonsoft.com/json/help/html/CustomJsonConverterGeneric.htm) to do this. – stuartd Apr 16 '19 at 23:06
  • @AndrewE have added the JSON and exception now – Moffen Apr 16 '19 at 23:09
  • looks like it might be because it doesnt know that L & 1 are two different objects. – traveler3468 Apr 16 '19 at 23:16
  • @AndrewE it doesn't, but it knows that it's supposed to be a Road object. I was thinking there might be some way "L1" be redirected to create a Road object through the constructor Road(string) ? – Moffen Apr 16 '19 at 23:20
  • 1
    have you try casting it? – traveler3468 Apr 16 '19 at 23:20
  • What do you mean by casting it? I think the only thing I can call when deserializing is the actual deserialize method, I can't cast any data types or execute any user code while it's running because it's all internal. – Moffen Apr 16 '19 at 23:22
  • Can't you just deserialise to an older version of the class when required? – DavidG Apr 16 '19 at 23:29
  • @DavidG an ugly hack but would work. I will include that in my code for the meantime but don't want to confusingly keep to versions of the same class as a long term solution. – Moffen Apr 16 '19 at 23:52
  • There's nothing wrong with multiple versions of the class if you have multiple versions of your data. – DavidG Apr 16 '19 at 23:53
  • @DavidG unfortunately it's not feasible in this project – Moffen Apr 16 '19 at 23:59

3 Answers3

1

As stuartd suggest, you could use a custom JsonConverter to explicitly say how to convert a string to your data type.

void Main()
{
    string json = @"{""RoadRef"":""L1""}";
    var data = JsonConvert.DeserializeObject<Foo>(json);
}

public class RoadConverter : JsonConverter<Road>
{

    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, Road value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override Road ReadJson(JsonReader reader, Type objectType, Road existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if (reader.ValueType != typeof(string))
            return (Road)serializer.Deserialize(reader, objectType);

        var data = reader.Value as string;
        return new Road(data);
    }
}


public class Foo
{
    [JsonConverter(typeof(RoadConverter))]
    public Road RoadRef { get; set; }
}

public class Road
{
    public Road(string val)
    {
        Lane = val[0];
        Number = int.Parse(val.Substring(1));
    }

    public char Lane { get; set; }
    public int Number { get; set; } = 1;
}

If you don't want to use Attribute decorator and keep your model out of any serialization concern, you can use it as parameter of DeserialzeObject method

var data = JsonConvert.DeserializeObject<Foo>(json, new RoadConverter());
Kalten
  • 4,092
  • 23
  • 32
1

Rather than using a custom JsonConverter you could use the implicit operator to convert directly from a string:

public class Road
{
    public Road(string val)
    {
        Lane = val[0];
        Number = int.Parse(val.Substring(1));
    }

    public char Lane { get; set; }
    public int Number { get; set; } = 1;

    public static implicit operator Road(string val)
    {
        return new Road(val);
    }
}
DavidG
  • 113,891
  • 12
  • 217
  • 223
0

My solution was to add an overriding cast from string to the class. The JSON Deserialize method tries to explicitly cast the string value to the class, which is now possible.

public Road(string val)
{
    Lane = val[0];
    Number = int.Parse(val.Substring(1));
 }

public static explicit operator Road(string val)
{
    return new Road(val);
}
Moffen
  • 1,817
  • 1
  • 14
  • 34