1

Overview :
I'm trying to make an app that deserializes a local JSON to a List that is then converted to a DataTable, which is displayed into a DataGridView.

The data I'm working with is for a trading cards game JSON contains a list of cards and each card object get a bunch properties, lang, name etc...
There will be more details on the datasource below.

Problem :
I've a semi-working project but I can't figure out why some properties and data values are not passing into my List at deserialization as soon as objects are not RootObjectLevel.

Every time an abject is encapsulated with { } deeper than root level such as  legalities in the code below, my DataGridView just shows one column with (Collection) or namespace.classname instead of all the columns of properties.

Sample shrunken JSON code to describe the above (full JSON available after)

{
  "lang": "en",
  "set": "lea",
  "set_name": "Limited Edition Alpha",
  "collector_number": "142",
  "name": "Dwarven Demolition Team",
  "legalities": {
    "standard": "not_legal",
    "future": "not_legal",
    "historic": "not_legal",
    "gladiator": "not_legal",
    "pioneer": "not_legal",
    "modern": "legal",
    "legacy": "legal",
    "pauper": "not_legal",
    "vintage": "legal",
    "penny": "not_legal",
    "commander": "legal",
    "brawl": "not_legal",
    "duel": "legal",
    "oldschool": "legal",
    "premodern": "not_legal"
  },
}

A full JSON example that is returned for a card so you can see the entire thing:
https://api.scryfall.com/cards/03482c9c-1f25-4d73-9243-17462ea37ac4

You have the option to see the Raw Data and even format it so its humanly readable like above.

JSON data source – https://scryfall.com/docs/api/bulk-data

I'm using the « Default Cards » and « All Cards » Files, the second one being 1,2Go

Goal :
The normal behavior or the result I want to achieve is displaying all the 15 different objects that are in "legalities": { } as headers

| standard | future | historic | etc...

and the legal or not legal value that goes with it for each cards on each row.

Instead of having this result : Current Result Screenshot from running my code

My guess :
I do not have any errors it works for everything that is RootObject but data deeper than RootObjectLevel are just returning null or are not populating the list properly somehow.

I suspect I need a custom converter, a token reader, maybe using a Dictionary or something along those lines but after looking for a week I'm just clueless, I'm just too new to C# I guess, its been like 2 weeks since I started coding.

I'm not even sure I would be able to remember all the things I tired so far, and list them here but there are not that many links that show up as "non read" when I try to once more make a new search to fix my problem.

So far I've been able to find what I need on Google, but this time I need some help.

Now the CODE :
pictureBox_Click to open my local JSON and write its path to a Label.Text

private void pictureBoxScryfallOpenFile_Click(object sender, EventArgs e)
{
    using (var openFileDialog = new OpenFileDialog())
    {
        openFileDialog.Title = "Please select a JSON file";
        openFileDialog.Filter = "JSON files (*.json) | *.json";
        openFileDialog.RestoreDirectory = true;

        if (openFileDialog.ShowDialog() == DialogResult.OK)
        {
            string fileName = openFileDialog.FileName;

            labelScryfallOpenedFile.Visible = true;
            labelScryfallOpenedFile.Text = fileName;
        }
    }
}

Then I'm grabbing the path from the Label.Text with label.Text.ToString() on a new buttom_Click to start deserializing.

Along with that I also have a ComboBox with 4 choices, each of them under a If call to trigger the good Class that have the JSONProperty I want to read from the file

Then I deserialize my list of objects as a List, which is converted to a DataTable that is set as DataSource for the DataGridView.

I'm shrinking the code with no ComboBox and minimal code to reproduce the problem.

using Newtonsoft.Json;

public void buttonReadScryfall_Click(object sender, EventArgs e)
{
    if (labelScryfallOpenedFile.Visible == true)
    {
        string jsonFilePath = labelScryfallOpenedFile.Text.ToString();

        using (var jsonFile = new StreamReader(jsonFilePath))
        {                   
            var jsonTextReader = new JsonTextReader(jsonFile);

            var serializer = new JsonSerializer();
            
            var cardLegalities = serializer.Deserialize<List<CardLegalities>>(jsonTextReader);

            DataTable dtLegalities = ConvertToDataTable(cardLegalities);

            dataGridView.DataSource = null;
            dataGridView.DataSource = dtLegalities;
            dataGridView.Refresh();                  
        }
    }
    else
    {
        MessageBox.Show("!!! No File Selected !!!"
                        + Environment.NewLine +
                        "Open a Local File Using the Appropriate Folder Icon"
                        + Environment.NewLine +
                        "Before Attempting to Deserialize a JSON");
    }
}

Note:
I need that StreamReader, it's even mandatory in that case, if I use a standard reader.ReadAllText() it ends up with an Out of Memory exception as the biggest file I'm working with is 1.2Go and we're talking about 300k+ rows with over a hundred of columns if I decide to go with the entire JSON properties that are available for each card.

I also need to have a conversion to a DataTable or else I'm not able to setup a search filter with DefaultView.RowFilter.

ConvertToDatable

public static DataTable ConvertToDataTable<T>(IList<T> data)
{
    PropertyDescriptorCollection properties =
        TypeDescriptor.GetProperties(typeof(T));
    
    DataTable table = new DataTable();

    foreach (PropertyDescriptor prop in properties)
        table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
    foreach (T item in data)
    {
        DataRow row = table.NewRow();
        foreach (PropertyDescriptor prop in properties)
            row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
        table.Rows.Add(row);
    }
    return table;
}

And finally the Class that goes with it:

using Newtonsoft.Json;

public class CardLegalities
{
    [JsonProperty("lang")]
    public string Lang { get; set; }

    [JsonProperty("set")]
    public string Set { get; set; }

    [JsonProperty("set_name")]
    public string SetName { get; set; }

    [JsonProperty("collector_number")]
    public string CollectorNumber { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    /// <summary>
    /// An object describing the legality of this card in different formats
    /// </summary>
    [JsonProperty("legalities")]
    public Legalities Legalities { get; set; }
}

public class Legalities
{
    [JsonProperty("standard")]
    public string Standard { get; set; }

    [JsonProperty("future")]
    public string Future { get; set; }

    [JsonProperty("historic")]
    public string Historic { get; set; }

    [JsonProperty("gladiator")]
    public string Gladiator { get; set; }

    [JsonProperty("pioneer")]
    public string Pioneer { get; set; }

    [JsonProperty("modern")]
    public string Modern { get; set; }

    [JsonProperty("legacy")]
    public string Legacy { get; set; }

    [JsonProperty("pauper")]
    public string Pauper { get; set; }

    [JsonProperty("vintage")]
    public string Vintage { get; set; }

    [JsonProperty("penny")]
    public string Penny { get; set; }

    [JsonProperty("commander")]
    public string Commander { get; set; }

    [JsonProperty("brawl")]
    public string Brawl { get; set; }

    [JsonProperty("duel")]
    public string Duel { get; set; }

    [JsonProperty("oldschool")]
    public string Oldschool { get; set; }

    [JsonProperty("premodern")]
    public string Premodern { get; set; }
}

I'm really curious to know how fix the problem, I'm pretty sure I'll see something that I've seen in my Google searches but unable to adapt to my needs since I'm just too new in C# coding.
I could eventually push the app to GitHub for you to clone the repository if that is more convenient.

Jimi
  • 29,621
  • 8
  • 43
  • 61
Carver
  • 19
  • 3
  • Question: why DataTable? Data binding works to typed data, and serializers work better with typed data. A `List` for some `Foo` usually works much better... – Marc Gravell Jul 11 '21 at 21:08
  • With or Without convertion, the result displayed in dataGridVew is the same, the only thing changing is with it i can use DefaultView.RowFilter and without ive NullReferenceException and i need to find another solution for filtering results. – Carver Jul 11 '21 at 22:13
  • Noted, DataTable does have some additional support for filtering and sorting. Suggestion: deserialize with List-Foo, and then translate that list to populate a DataTable as a separate step. A cheeky (lazy) way here might be to Fill the DataTable from the IDataReader that you can get from FastMember, which has an API to view List-T etc as readers – Marc Gravell Jul 11 '21 at 22:17
  • Do you want the properties at the root level to appear, or just the properties under `"legalities"`? Also, what casing do you want for your column names -- the casing in the JSON file, or the casing for your c# property names? – dbc Jul 11 '21 at 22:37
  • i would like the properties at root level + the properties under legalities. If you can check back the image link to a screenshot that ive attached to the post (in Goal section), Basically i would want that, but with the legalities column removed and replaced by all the properties under "legalities" 1 column per property. For column names casing i'd prefer CollectorNumber and not collector_number but tbh that is not super important. cheers – Carver Jul 11 '21 at 22:56
  • You could use the `JsonPathConverter` described in the second part of [this answer](https://stackoverflow.com/a/33094930/10263) to allow you to move all the `legalities` properties into your root class. You would just need to add the `[JsonConverter(typeof(JsonPathConverter))]` attribute to the class, and then prefix all the `JsonProperty` names for the nested properties with `legalities.`. For example `[JsonProperty("legalities.standard")]`. – Brian Rogers Jul 12 '21 at 05:39
  • @Brian, Thank you very much, that did the trick just like i wanted. Should i be editing my original post keeping all of the original post and adding a new section (Resolved or Updated) with a link to your post as source, aswell as adding 2 new code blocks 1 with modified class and 1 with your pathconverter code so people can see the before and after? or maybe make a completly separate answer with those infos so its seperated from original post? or is your post in those replies enough? Thank you again Cheers – Carver Jul 12 '21 at 19:55
  • If you want to post a resolution to your question, the best way to do that is to post it as an answer. It is always OK to answer your own question. Conversely, it is generally frowned upon to edit the question and post the resolution there. All that being said, since the resolution was essentially the same as the other question I linked, I could just close this one as a duplicate of the other unless you feel there are other details you'd like to share which make it different enough that others might find helpful. Your call. – Brian Rogers Jul 12 '21 at 20:26
  • I would have 1 more question before we call it, if you can give me a hand on this too that would be awsome. there are other properties that are within an array like "colors":["R"],"color_identity":["R"], they are at rootobject too, they can be null or have more than 1 value like so "colors":["G","U","W"], im trying to call them with [JsonProperty("colors")] public string[] Colors { get; set; } but nothing return not even the column header. does the converter handle those aswell? or it will need adjustments? If converter is all fine, may i have a hand on why it not returning data. ty in advance – Carver Jul 13 '21 at 00:32
  • adding an index[0] seems to works. like changing [JsonProperty("multiverse_ids")] public string[] MultiverseIds { get; set; } TO [JsonProperty("multiverse_ids[0]")] public string MultiverseIds { get; set; } In that case that works because there will always be only 1 value in that array. However for the colors and color_identity i posted in my last message its doesnt works, i mean, it only grabs the 1st value, if there are multiple it doesnt show them all. Any clues? im continuing to look on that in the meantime. Cheers – Carver Jul 13 '21 at 23:51
  • `DataGridView` doesn't support array columns, so you need to convert any arrays to simple strings to get them to show up. I've added an answer explaining this. – Brian Rogers Jul 17 '21 at 05:23

2 Answers2

1

To answer your main question, you could use the JsonPathConverter described in the second part of this answer to allow you to move all the Legalities properties into your CardLegalities class to make it easier to handle. You would just need to add the [JsonConverter(typeof(JsonPathConverter))] attribute to the CardLegalities class, and then prefix all the JsonProperty names for the nested properties with legalities., for example [JsonProperty("legalities.standard")].

This will get you most of the way there. However, you mentioned in the comments that some of your properties are arrays, such as colors, and you are not seeing any output for those in your DataGridView.

The problem here is that the DataGridView control ignores array columns when binding to a DataTable. You can test this easily enough with the following code:

DataTable dt = new DataTable();
dt.Columns.Add("Simple String", typeof(string));
dt.Columns.Add("Simple Int", typeof(int));
dt.Columns.Add("Array of String", typeof(string[]));
dt.Rows.Add("Foo", 25, new string[] { "A", "B" });

dataGridView1.DataSource = null;
dataGridView1.DataSource = dt;
dataGridView1.Refresh();

If you run the above code in a new Form containing a DataGridView, you'll see the following output, with the "Array of String" column conspicuously missing:

Test code screenshot

What this means is that you'll need to convert your array properties into simple strings (by concatenating the values) when you are doing your data table conversion. Here is an updated version of your ConvertToDataTable<T>() method which should do the trick:

public static DataTable ConvertToDataTable<T>(IList<T> data)
{
    PropertyDescriptorCollection properties =
        TypeDescriptor.GetProperties(typeof(T));

    DataTable table = new DataTable();

    foreach (PropertyDescriptor prop in properties)
    {
        Type dataType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
        if (dataType.IsArray) dataType = typeof(string);
        table.Columns.Add(prop.Name, dataType);
    }

    foreach (T item in data)
    {
        DataRow row = table.NewRow();
        foreach (PropertyDescriptor prop in properties)
        {
            object val = prop.GetValue(item);
            if (val != null && prop.PropertyType.IsArray)
            {
                var items = ((IEnumerable)val)
                    .Cast<object>()
                    .Where(o => o != null)
                    .Select(o => o.ToString());

                val = string.Join(", ", items);
            }
            row[prop.Name] = val ?? DBNull.Value;
        }
        table.Rows.Add(row);
    }
    return table;
}
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • Hello,, thank you for the code, i knew there was a convertion problem but couldnt fix code myself. Just a note on the code, it needs 2 assemblies "using System.Collections;" for ((IEnumerable)val) and "using System.Linq;" for .Cast() or you'll get exceptions. I tought i had mention it if some other novice people like me are using the code. PS I had a working hard coded way using 1 colum for each index and then cloning to group things up and hide unnecessary columns, but with this one it will be a lot cleaner, ty so much! Cheers. – Carver Jul 17 '21 at 15:39
  • No problem; glad I could help. – Brian Rogers Jul 17 '21 at 21:41
0

You could also give a try Cinchoo ETL, an open source library for this type of conversion

Here is the sample code

using ChoETL;

ChoETLSettings.NestedKeySeparator = '.';
using (var r = new ChoJSONReader<CardLegalities>("*** YOUR JSON FILE PATH ***")
    )
{
    var dt = r.AsDataTable();
    Console.WriteLine(dt.Dump());
}

Disclaimer: I'm author of this library

Cinchoo
  • 6,088
  • 2
  • 19
  • 34