2

I have a c# struct that is used as dictionary key. In order to make that dictionary convert to json I need struct serialized to string (like json.net does for built in structs).

public struct CreditRating
{
    public CreditRating(string json) : this()
    {
        var levels = json.Split(new[] { '~' }, StringSplitOptions.None);
        if (levels.Count() >= 3) Level3 = levels[2];
        if (levels.Count() >= 2) Level2 = levels[1];
        if (levels.Any()) Level1 = levels[0];
    }

    public string Level1 { get; set; }
    public string Level2 { get; set; }
    public string Level3 { get; set; }

    public override string ToString()
    {
        return string.Format("{0}~{1}~{2}", Level1, Level2, Level3);
    }

    public static CreditRating Parse(string json)
    {
        return new CreditRating(json);
    }
}

And my test:

        var rating = new CreditRating { Level1 = "first", Level2 = "Sergey" };
        var ratingJson = JsonConvert.SerializeObject(rating); // {"Level1":"first","Level2":"Sergey","Level3":null}
        var rating2 = JsonConvert.DeserializeObject<CreditRating>(ratingJson);

        var dict = new Dictionary<CreditRating, double> {{rating, 2d}};
        var dictJson = JsonConvert.SerializeObject(dict); //{"first~Sergey~":2.0}
        var failingTest = JsonConvert.DeserializeObject<Dictionary<CreditRating, double>>(dictJson);

The last statement fails as it does not call into my Parse method or the public constructor. I followed the documentation but can't get pass this.

Zvirk
  • 199
  • 3
  • 7
  • 2
    You shouldn't normally use `struct`... Unless you have particular problems, you should use `class`. And try not to use `Count()` when you can use `Length` – xanatos May 22 '15 at 10:17
  • 1
    Structs always have a public parameterless constructor which is why Json.Net is able to deserialize the struct at all. Why do you expect the deserializer to call your methods though, and why do you need them? The `Level` properties should already be serialized – Panagiotis Kanavos May 22 '15 at 10:24
  • See http://stackoverflow.com/a/7010231/613130 There is a limitation with Json.NET and Dictionary keys – xanatos May 22 '15 at 10:31
  • @PanagiotisKanavos I think that perhaps he wants to use a custom formatter for serialization... To serialize it as `Level1~Level2~Level3` instead of serializing the properties. – xanatos May 22 '15 at 10:32
  • I should probably add that the dictionary is a property of another class, if that makes any diffrence – Zvirk May 22 '15 at 10:46
  • Structs maintain uniquenes of keys in dictionary out of the box, but the question still remains: it dictionary is serialized to {"first~Sergey~":2.0} why can't I deserialize it back. I tried using JsonConverters but even that is screwy. If I decorate my dictionary property like: – Zvirk May 22 '15 at 10:53
  • [JsonConverter(typeof(CreditRatingDoubleDictionaryConverter))] public Dictionary ShiftsByRating { get; set; } It does not get called – Zvirk May 22 '15 at 10:53

1 Answers1

1

Ok, so after trying a lot of things this worked in the end - In case anyone else bumps into this:

[DataContract(Namespace = ContractNamespace.Current)]
public class CreditSpreadShiftWithLevels
{
    [OnDeserializing]
    private void Initialize(StreamingContext ctx)
    {
        ShiftsByRating = new Dictionary<CreditRating, double>();
    }
    [DataMember]
    public bool SplitByRating { get; set; }

    [DataMember]
    public double ShiftValue { get; set; }

    [DataMember]
    [JsonConverter(typeof(CreditRatingDoubleDictionaryConverter))]
    public Dictionary<CreditRating, double> ShiftsByRating { get; set; }

    //other properties

}

[DataContract(Namespace = ContractNamespace.Current)]
public struct CreditRating
{
    public CreditRating(string json): this()
    {
        var levels = json.Split(new[] { '~' }, StringSplitOptions.None);
        var cnt = levels.Length;
        if (cnt >= 3) Level3 = levels[2];
        if (cnt >= 2) Level2 = levels[1];
        if (cnt >= 1) Level1 = levels[0];
    }

    [DataMember]
    public string Level1 { get; set; }
    [DataMember]
    public string Level2 { get; set; }
    [DataMember]
    public string Level3 { get; set; }

    public override string ToString()
    {
        return string.Format("{0}~{1}~{2}", Level1, Level2, Level3);
    }
}


public class CreditRatingDoubleDictionaryConverter : JsonConverter
{
    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)
    {
        var dict = new Dictionary<CreditRating, double>();
        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.PropertyName)
            {
                string readerValue = reader.Value.ToString();
                var cr = new CreditRating(readerValue);
                if (reader.Read() && reader.TokenType == JsonToken.Float)
                {
                    var val = Convert.ToDouble(reader.Value);
                    dict.Add(cr, val);
                }
            }
            if (reader.TokenType == JsonToken.EndObject) return dict;
        }
        return dict;
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Dictionary<CreditRating, double>).IsAssignableFrom(objectType);
    }
}

In short I created converter for the dictionary (and not struct) that was giving me grief and attributed property of the parent class. This made json.net call into my custom logic when deserializing. There is something already built into the library that makes dictionary serialization call into struct's ToString when creating dictionary key (this makes it slightly inconsistent as it does not respect the return route even though documentation sort of suggests it - http://docs.servicestack.net/text-serializers/json-serializer)

One pain point with this that I need to provide separate converter for every different type of dictionary that is using the struct for its key e.g.

public Dictionary<CreditRating, List<string>> BucketsByRating { get; set; }

will need another converter. I need to see if generics can be used to increase reuse but it would have been much better if I could provide single converter for struct that would be picked up for all different dictionary properties that I have.

In any case I hope this comes useful and saves someone some time.

Thanks to all who offered advice, much appriciated

Zvirk
  • 199
  • 3
  • 7