0

I'm trying to write a recipe parser in C#. I have tried using both Newtonsoft and System.Text.Json. I have used online json class builders and VS2019 Paste as json classes. I have got the Newtonsoft example code to run.

Schema.org Recipe https://schema.org/Recipe

Class I am using. I'm really only interested in these 2 values

public class Recipe
{
public string recipeIngredient { get; set; }
public string recipeInstructions { get; set; }
}

Code - this code runs with with no error.

var document = File.ReadAllText(@"D:\recipeExample.json");
Recipe recipe = null;
try
{
    recipe = System.Text.Json.JsonSerializer.Deserialize<Recipe>(document);
}
    catch (System.Text.Json.JsonException ex)
{
    Console.WriteLine($"Error parsing : {ex}");
}

Trying to read value. No error or value.

Console.WriteLine(recipe.recipeIngredient);

I'm not sure I understand how a schema.org document needs to be traversed. In the generated classes I can see Graph and Context which I'm guessing are root nodes. But my experiments suggest that I don't need all of the classes - just recipeIngredient and recipeInstructions. And this is only one recipe - I want to parse a list of recipes and I'm sure they'll all have their own classes so I'm looking for the most generalised way to get the 2 field values I'm seeking.

EDIT

Using tymtam's Json Document example I can see that there's an object there with

Console.WriteLine(recipes.Count);

But I can't get the values to display using

Console.WriteLine(recipes[0].Ingredients.ToString());
Console.WriteLine(recipes[0].Instructions.ToString());

I also tried to print ingredients with

foreach (var recipe in recipes)
{
Console.WriteLine(recipes[0].Ingredients);
}

But that only prints the object name and not the elements

System.Linq.Enumerable+WhereSelectEnumerableIterator

EDIT 2: SOLVED

foreach (var ingredient in recipes[0].Ingredients)
   Console.WriteLine(ingredient);

foreach (var instruction in recipes[0].Instructions)
   Console.WriteLine(instruction);
Corpuscular
  • 113
  • 6

2 Answers2

2

The relevant parts of json are the following:

{
   "@context":"https://schema.org",
   "@graph":[
      (...)
      {
         "@type":"Recipe",
         "recipeIngredient":[
            "12 oz rice noodles",
            (...)
         ],
         "recipeInstructions":[
            {
               "@type":"HowToStep",
               "text":"Bring ...",
               "name":"Bring ...",
               "url":"https://..."
            },
            (...)
         ],
  (...)

System.Text.Json with JsonDocument

using (JsonDocument doc = JsonDocument.Parse(json))
{
    JsonElement root = doc.RootElement;
    JsonElement graph = root.GetProperty("@graph");

    var recipes = graph.EnumerateArray()
    .Where(g => g.GetProperty("@type").GetString() == "Recipe")
    .Select(r => new
    {
        Ingredients = r.GetProperty("recipeIngredient").EnumerateArray().Select(p => p.GetString()),
        Instructions = r.GetProperty("recipeInstructions").EnumerateArray().Select(p => new
        {
            @type = p.GetProperty("@type").GetString(),
            text = p.GetProperty("text").GetString(),
            name = p.GetProperty("name").GetString(),
            url = p.GetProperty("url").GetString(),
        })
    })
    .ToList();
}

System.Text.Json with classes

class Root 
{
    [JsonPropertyName("@context")]
    public string Context {get; set;}
    
    [JsonPropertyName("@graph")]
    public List<Element> Graph {get; set;}
}


class Element {
    [JsonPropertyName("@type")]
    public string Type {get;set;}
    public List<string> RecipeIngredient {get; set;}

    public List<Instruction> RecipeInstructions {get; set;}
}

class Instruction {
    public string Text {get; set;}
}
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
var x = JsonSerializer.Deserialize<Root>(json, options);
var recipes = x.Graph.Where( o => o.Type == "Recipe");

Json.Net

Please see the official Querying JSON with LINQ or Querying JSON with SelectToken.

Here is a mix and match solution with querying and classes:

using Newtonsoft.Json.Linq;

JObject o = JObject.Parse(json);
JToken t = o.SelectToken("$.@graph[?(@.@type == 'Recipe')]");
var recipe = t.ToObject<Recipe>();

(...)
class Recipe {
    public List<string> RecipeIngredient {get; set;}

    public List<Instruction> RecipeInstructions {get; set;}
}

class Instruction {
    public string Text {get; set;}
}

For multiple recipes use SelectTokens.

tymtam
  • 31,798
  • 8
  • 86
  • 126
  • Thanks @tymtam Can you tell me how to enumerate the lists? I have run the first two examples without error but can't work out what/how to print the list to console. – Corpuscular Apr 14 '21 at 22:43
  • You can just serialise it back: `Console.WriteLine(JsonSerializer.Serialize(recipes, new JsonSerializerOptions {WriteIndented = true}));` – tymtam Apr 16 '21 at 02:03
0

I think your problem is that you're reading the entire file at once. so the deserialize method may not be able to assign the individual values because your trying to parse one variable from all the text

if you're using System.Text.Json you should be able to set 2 properties in your class,

public string recipeIngredient {get; set;}
public string recipeInstructions {get; set;}
public override string ToString() => JsonSerializer.Serialize<Recipe>(this);

and then when you deserialize you can do something like this

public list<Recipe> getRecipe()
{
   using (var jsonFileReader = File.OpenText(filename))
   {
      return JsonSerializer.Deserialize<Recipe>(jsonFileReader.ReadToEnd());
   }
}

with this you should be able to make a list with the type being your recipe class. and then you should be able to reference the list. (edit) I forgot to mention it but you would probably want to call it, to do that you would say list<Recipe> recipeList = getRecipe(); and then you would just iterate through the list and each object would have a ".recipeIngredient" and ".recipeInstructions" that you could use to format the output

  • Thank you for answering but I don't understand what you are suggesting. I copied your code List should be uppercase L? Then got the error: Cannot implicitly convert type 'WindowsFormsApp1.Form1.Recipe' to 'System.Collections.Generic.List – Corpuscular Apr 14 '21 at 04:21
  • I would suggest making a new class that isn't tied to a form and having the be your Recipe class with the two fields. your problem seems to be that the Recipe class is referencing a form. – Ross Parker Apr 14 '21 at 04:25
  • " a new class that isn't tied to a form" - it is a WinForms app. I created a new Recipe class within the App. Is that what you meant? Not sure where I should put the list getRecipe() - I'm still getting the same error on both the form and in the class. I also added getRecipe(string filename) – Corpuscular Apr 14 '21 at 04:34
  • The reason it said "Cannot implicitly convert type 'WindowsFormsApp1.Form1.Recipe'" i'm assuming is because the override method uses "this" when its telling the method what its serializing. but in your case the instance of "this" that its addressing seems to be referring to a form. maybe I misunderstood what you're trying to do – Ross Parker Apr 14 '21 at 04:38
  • and would you mind linking the content of the json file? I can try writing the code here to see what the problem is – Ross Parker Apr 14 '21 at 04:39
  • No worries thank you for your suggestions – Corpuscular Apr 14 '21 at 04:40
  • Thanks https://drive.google.com/file/d/1wS-CnbPjyi6N77WB0TxEVrUsTQypR2q3/view?usp=sharing – Corpuscular Apr 14 '21 at 04:45