-1

I have a very similar issue to this question here, except my application is in C#, and I can't figure out how to convert the solution unfortunately. I am trying to deserialize a JSON result that looks like this:

"error":[],
"result":
     {
        "MANAEUR":[
                [1619042400,"1.11200","1.13488","1.08341","1.10077","1.09896","58878.56534370",137],
                [1619046000,"1.09767","1.12276","1.08490","1.11097","1.10456","25343.25910419",77],

            ],
        
        "last":1619118000
    }

I use the following classes:

public class ResponseBase
{
    [JsonProperty(PropertyName = "error")]
    public List<string> Error;
}

public class OHLCResponse : ResponseBase
{
    [JsonProperty("result")]
    public OHLCResult Result;
}

public class OHLCResult
{
    [JsonProperty("pair_names")]
    public Dictionary<string, OHLC[]> GetHistory;

    [JsonProperty("last")]
    public long Last;
}

.... and then finally the guts of it:

public class OHLC
{
    public int Time;
    public decimal Open;
    public decimal High;
    public decimal Low;
    public decimal Close;
    public decimal Vwap;
    public decimal Volume;
    public int Count;
}

I have a standard deserializer class which works for all other calls I am using to the same API, but I cannot get this call to work. When I retrieve OHLCResponse object,I don't get an error, and "Result.Last" is always populated, but the expected array of OHLC items in "Result.GetHistory" is always empty/null. I know that the data has been returned successfully since I can see the data in the variable returned from the WebRequest that I am then passing to the deserializer function, so it must be that I have these classes laid out incorrectly I guess.

Can anyone see what I'm doing wrong?

Many thanks in advance, Dave

Davy C
  • 639
  • 5
  • 16
  • 1) The JSON data says `MANAEUR` but you have `pair_names` in your model. 2) `MANAEUR` is an array of arrays. Not a dictionary of arrays. 3) You have `OHLC` as an object, but it is not an object. it's an array; There are a few ways to make this work. You could change `MANAEUR` property to `List>` then do a `.Select()` to the `OHLC` object, or you could use a custom [JsonConverter](https://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm). – Andy Apr 22 '21 at 23:30
  • Thanks for your comments Andy. – Davy C Apr 22 '21 at 23:46
  • I commented out the property tag in "OHLCResult" and then changed the property to "public List> GetHistory;", but this didn't change the outcome unfortunately. I appreciate your input though. I don't understand what you mean by do a ".Select()". – Davy C Apr 22 '21 at 23:55
  • I am assuming you are using Newtonsoft.Json? Not System.Text.Json? – Andy Apr 23 '21 at 00:04
  • Yes. I have "using Newtonsoft.Json" declared in the model class. – Davy C Apr 23 '21 at 00:39
  • I will throw something together -- give me a few minutes – Andy Apr 23 '21 at 00:44

2 Answers2

1

Your JSON is not valid, I had to modify it to make it valid JSON (https://jsonformatter.org/). I added the root brackets and removed the comma delimter after the second inner array entry.

Valid JSON:

{
  "error":[],
  "result":
     {
        "MANAEUR":[
                [1619042400,"1.11200","1.13488","1.08341","1.10077","1.09896","58878.56534370",137],
                [1619046000,"1.09767","1.12276","1.08490","1.11097","1.10456","25343.25910419",77]
            ],
        
        "last":1619118000
    }
}

After updating the JSON, I used Visual Studio's 'Paste Special' to generate C# objects from the JSON. The following classes were created.

public class RootObject
{
    [JsonProperty("error")]
    public object[] Error { get; set; }

    [JsonProperty("result")]
    public Result Result { get; set; }
}

public class Result
{
    [JsonProperty("MANAEUR")]
    public object[][] Manaeur { get; set; }

    [JsonProperty("last")]
    public int Last { get; set; }
}

With the above JSON and classes, I used the following to deserialize the JSON.

string json = "{\"error\":[],\"result\":{\"MANAEUR\":[[1619042400,\"1.11200\",\"1.13488\",\"1.08341\",\"1.10077\",\"1.09896\",\"58878.56534370\",137],[1619046000,\"1.09767\",\"1.12276\",\"1.08490\",\"1.11097\",\"1.10456\",\"25343.25910419\",77]],\"last\":1619118000}}";
var obj = JsonConvert.DeserializeObject<RootObject>(json);

EDIT: To handle the MANAEUR property where the key label can be different.

Create a JsonConverter...

public class ManaeurConverter : JsonConverter
{
    private Dictionary<string, string> propertyMappings { get; set; }
       
    public ManaeurConverter()
    {
        this.propertyMappings = new Dictionary<string, string>
        {
            {"NOTMANAEUR","MANAEUR"}
        };
    }

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        object instance = Activator.CreateInstance(objectType);
        var props = objectType.GetTypeInfo().DeclaredProperties.ToList();

        JObject jo = JObject.Load(reader);
        foreach (JProperty jp in jo.Properties())
        {
            if (!propertyMappings.TryGetValue(jp.Name, out var name))
                name = jp.Name;

            PropertyInfo prop = props.FirstOrDefault(pi =>
                    pi.CanWrite && pi.GetCustomAttribute<JsonPropertyAttribute>().PropertyName == name);

        prop?.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
        }
        return instance;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType.GetTypeInfo().IsClass;
    }

    public override bool CanWrite => false;
}

... Add the JsonConverter attribute to the class...

[JsonConverter(typeof(ManaeurConverter))]
public class Result
{
    [JsonProperty("MANAEUR")]
    public object[][] Manaeur { get; set; }

    [JsonProperty("last")]
    public int Last { get; set; }
}

... and parse like so...

string json_Manaeur = "{\"error\":[],\"result\":{\"MANAEUR\":[[1619042400,\"1.11200\",\"1.13488\",\"1.08341\",\"1.10077\",\"1.09896\",\"58878.56534370\",137],[1619046000,\"1.09767\",\"1.12276\",\"1.08490\",\"1.11097\",\"1.10456\",\"25343.25910419\",77]],\"last\":1619118000}}";
string json_Not_Manaeur = "{\"error\":[],\"result\":{\"NOTMANAEUR\":[[1619042400,\"1.11200\",\"1.13488\",\"1.08341\",\"1.10077\",\"1.09896\",\"58878.56534370\",137],[1619046000,\"1.09767\",\"1.12276\",\"1.08490\",\"1.11097\",\"1.10456\",\"25343.25910419\",77]],\"last\":1619118000}}";
            
var objManaeur = JsonConvert.DeserializeObject<RootObject>(json_Manaeur);
var objNotManaeur = JsonConvert.DeserializeObject<RootObject>(json_Not_Manaeur);
quaabaam
  • 1,808
  • 1
  • 7
  • 15
  • That's great. Many thanks. Sorry, I must have chopped off the { } characters when I posted my JSON code. This solution works, brilliant, but the "MANAEUR" field is dynamic and that label changes. I can't explicity tag that as the JsonProperty. Is there a way round this that you know of? Many, many thanks for your help so far. – Davy C Apr 23 '21 at 01:24
  • This doesn't give you concrete access to the values of the array. I thought that's what you needed. – Andy Apr 23 '21 at 01:26
  • Turns out this is the only correct answer. OP didn't mention that the MANAEUR object is different every time. – Andy Apr 23 '21 at 14:37
  • @DavyC I've updated my answer to address issue where `MANAEUR` label can be different. – quaabaam Apr 23 '21 at 17:39
  • This is fantastic! Thank you so much. I wish I could upvote this as an answer. Just one thing. Do you know how I can de-couple this part so I can pass in the expected PropertyName as a variable? I will know what the expected value should be at runtime, so the dictionary key needs to be dynamic: {"NOTMANAEUR","MANAEUR"} – Davy C Apr 24 '21 at 15:10
1

The object you posted isn't valid JSON. The outside curly braces are missing. So I am going to assume it should look like this:

{
    "error": [],
    "result": {
        "MANAEUR": [
            [1619042400, "1.11200", "1.13488", "1.08341", "1.10077", "1.09896", "58878.56534370", 137],
            [1619046000, "1.09767", "1.12276", "1.08490", "1.11097", "1.10456", "25343.25910419", 77],

        ],
        "last": 1619118000
    }
}

Anonymous Deserialization

The first method you could do, which may be a tad kludgy since you have to deserialize twice, is use anonymous deserialization.

Let's start by defining some models:

public sealed class OHLCModel
{
    public long Time { get; set; }
    public decimal Open { get; set; }
    public decimal High { get; set; }
    public decimal Low { get; set; }
    public decimal Close { get; set; }
    public decimal Vwap { get; set; }
    public decimal Volume { get; set; }
    public int Count { get; set; }
}

public sealed class ResultModel
{
    [JsonIgnore]
    public IEnumerable<OHLCModel> Manaeur { get; set; }

    [JsonProperty("last")]
    public long Last { get; set; }
}

public sealed class RootModel
{
    [JsonProperty("error")]
    public List<string> Error { get; set; }

    [JsonProperty("result")]
    public ResultModel Result { get; set; }
}

As you can see we are ignoring the Manaeur object when serialization happens.

To make this method work, we'd do this:

var json = System.IO.File.ReadAllText(@"c:\users\andy\desktop\test.json");

// First, just grab the object that has the mixed arrays.
// This creates a "template" of the format of the target object
var dto = JsonConvert.DeserializeAnonymousType(json, new
{
    Result = new
    {
        Manaeur = new List<List<object>>()
    }
});
            
// Next, deserialize the rest of it
var fullObject = JsonConvert.DeserializeObject<RootModel>(json);

// transfer the DTO using a Select statement
fullObject.Result.Manaeur = dto.Result.Manaeur.Select(x => new OHLCModel
{
    Time = Convert.ToInt64(x[0]),
    Open = Convert.ToDecimal(x[1]),
    High = Convert.ToDecimal(x[2]),
    Low = Convert.ToDecimal(x[3]),
    Close = Convert.ToDecimal(x[4]),
    Vwap = Convert.ToDecimal(x[5]),
    Volume = Convert.ToDecimal(x[6]),
    Count = Convert.ToInt32(x[7])
});

This isn't the most ideal solution as you are tightly coupling to the model in a few spots. The ideal way to do this would be to make a custom JsonSerializer.

Use a Custom JsonConverter

First thing we do is change your ResultModel to look like this:

public sealed class ResultModel
{
    [JsonConverter(typeof(ManaeurJsonConverter)), JsonProperty("MANAEUR")]
    public IEnumerable<OHLCModel> Manaeur { get; set; }

    [JsonProperty("last")]
    public long Last { get; set; }
}

Then implement a JsonConverter:

public sealed class ManaeurJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) => false; // this will never get called

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var lst = JArray.Load(reader).ToObject<List<List<object>>>();
        return lst.Select(x => new OHLCModel
        {
            Time = Convert.ToInt64(x[0]),
            Open = Convert.ToDecimal(x[1]),
            High = Convert.ToDecimal(x[2]),
            Low = Convert.ToDecimal(x[3]),
            Close = Convert.ToDecimal(x[4]),
            Vwap = Convert.ToDecimal(x[5]),
            Volume = Convert.ToDecimal(x[6]),
            Count = Convert.ToInt32(x[7])
        });
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    { // we don't need to write
        throw new NotImplementedException();
    }
}

You can then simply call it as so:

var json = System.IO.File.ReadAllText(@"c:\users\andy\desktop\test.json");

var fullObject = JsonConvert.DeserializeObject<RootModel>(json);
Andy
  • 12,859
  • 5
  • 41
  • 56
  • That's great, I really appreciate you taking the time to write all that out. I will try it first thing in the morning. Many thanks. – Davy C Apr 23 '21 at 01:27
  • Hello Andy, the first solution gives me the data, many thanks. The second solution won't work, because the JSON property is dynamic: JsonProperty("MANAEUR") ......... "MANAEUR" is not static. I really appreciate your time and I've marked this as answered. – Davy C Apr 23 '21 at 13:00
  • I spoke to soon. The first suggestion doesn't work either upon closer inspection because "MANAEUR" is variable unfortunately. Damn. – Davy C Apr 23 '21 at 13:43
  • 1
    @DavyC -- your question says nothing about that. You even posted the `OHLC` model. That is slightly frustrating. You're only option at that point is to go with the other answer. – Andy Apr 23 '21 at 14:35
  • Hello Andy. Yes, apologies for that, I guess I made an assumption. Many, many thanks for your help. – Davy C Apr 24 '21 at 14:04