1

To work with a recipe webservice I'm developing, I have the following classes to hold and serialize recipe data:

class Recipe {

    public string RecipeId { get; private set; }

    public string UserId { get; set; }
    public string Title { get; set; }

    public IList<string> IngredientsList { get; set; }

    public List<Group<string, Ingredient>> IngredientsWithHeaders { get; set; }
    public List<Group<string, string>> InstructionsWithHeaders { get; set; }

    public List<string> Notes { get; set; }
    public ISet<string> Tags { get; set; }
    public int Yield { get; set; }

    public Recipe(string recipeId)
    {
        RecipeId = recipeId;
        IngredientsWithHeaders = new List<Group<string,Ingredient>>();
        InstructionsWithHeaders = new List<Group<string, string>>();
        IngredientsList = new List<string>();
    }

    public byte[] Image { get; set; }
}

class Ingredient
{
    public string Quantity { get; set; }
    public string Modifier { get; set; }
    public string Unit { get; set; }
    public string IngredientName { get; set; }
    public string Preparation { get; set; }

    public Ingredient(string[] line)
    {
        if (!string.IsNullOrWhiteSpace(line.ElementAt(0)))
        {
            Quantity = line.ElementAt(0);
        }
        if (!string.IsNullOrWhiteSpace(line.ElementAt(1)))
        {
            Unit = line.ElementAt(1);
        }
        if (!string.IsNullOrWhiteSpace(line.ElementAt(2)))
        {
            IngredientName = line.ElementAt(2);
        }
        if(line.Length>3)
        {
            Preparation = line.Last();
        }
    }
}

class Group<K, T> : ObservableCollection<T>
{
    public K Key { get; set; }

    public Group(K key, IEnumerable<T> items) : base(items)
    {
        Key = key;
        Debug.WriteLine(key);
    }
}    

The JSON output I am getting for the List<Group<string, Ingredient>> is

{
"IngredientsWithHeaders": [
    [
        {
            "Quantity": "3",
            "Modifier": null,
            "Unit": "tbsp",
            "IngredientName": "butter",
            "Preparation": null
        },
        {
            "Quantity": "1",
            "Modifier": null,
            "Unit": "16 oz. bag",
            "IngredientName": "marshmallows",
            "Preparation": null
        },
        {
            "Quantity": "2/3",
            "Modifier": null,
            "Unit": "cup",
            "IngredientName": "dry cake mix",
            "Preparation": null
        },
        {
            "Quantity": "6",
            "Modifier": null,
            "Unit": "cups",
            "IngredientName": "crispy rice cereal",
            "Preparation": null
        },
        {
            "Quantity": "1",
            "Modifier": null,
            "Unit": "container",
            "IngredientName": "sprinkles",
            "Preparation": "optional"
        }
        ]
    ]
}

and what I would like to be getting is more along the lines of

{
"IngredientsWithHeaders": [
    {
        "Group": {
            "Header": "BlankHeader",
            "Items": [
                {
                    "Quantity": "3",
                    "Modifier": null,
                    "Unit": "tbsp",
                    "IngredientName": "butter",
                    "Preparation": null
                },
                {
                    "Quantity": "1",
                    "Modifier": null,
                    "Unit": "16 oz. bag",
                    "IngredientName": "marshmallows",
                    "Preparation": null
                },
                {
                    "Quantity": "2/3",
                    "Modifier": null,
                    "Unit": "cup",
                    "IngredientName": "dry cake mix",
                    "Preparation": null
                },
                {
                    "Quantity": "6",
                    "Modifier": null,
                    "Unit": "cups",
                    "IngredientName": "crispy rice cereal",
                    "Preparation": null
                },
                {
                    "Quantity": "1",
                    "Modifier": null,
                    "Unit": "container",
                    "IngredientName": "sprinkles",
                    "Preparation": "optional"
                }
                ]
            }
        }
    ]
}

Do I need to write a custom serializer? If so, how do I go about casting an object to a parameterized Group without knowing if it is

Group<string, Ingredient> 

or

Group<string, string>

?

2
  • 1
    The JSON you want is invalid. Try uploading it to jsonlint.com and you will get an error. A JSON container can be either an object (an unordered set of name/value pairs) or an array -- but not both. Can you give an example of the JSON you want that is valid, according to jsonlint.com? Commented Aug 18, 2015 at 1:36
  • My mistake, edit with more specific JSON forthcoming. Commented Aug 18, 2015 at 14:42

1 Answer 1

2

The issue here is that your Group<K, T> is a collection that also has properties. Since a JSON container can either be an array (with no properties) or an object (with named key/value pairs), a collection with custom properties cannot be mapped automatically to either without data loss. Json.NET (and all other serializers AFAIK) choose to map the items not the custom properties.

You have a couple ways to deal with this:

  1. Write your own custom JsonConverter. You can determine the generic arguments using reflection along the lines of Json.Net returns Empty Brackets.

  2. Mark your Group<K, T> with [JsonObject].

The second option seems simplest, and would look like:

[JsonObject(MemberSerialization = MemberSerialization.OptIn)] // OptIn to omit the properties of the base class,  e.g. Count
class Group<K, T> : ObservableCollection<T>
{
    [JsonProperty("Header")]
    public K Key { get; set; }

    [JsonProperty("Items")]
    IEnumerable<T> Values
    {
        get
        {
            foreach (var item in this)
                yield return item;
        }
        set
        {
            if (value != null)
                foreach (var item in value)
                    Add(item);
        }
    }

    public Group(K Header, IEnumerable<T> Items) // Since there is no default constructor, argument names should match JSON property names.
        : base(Items)
    {
        Key = Header;
    }
}

Incidentally, you have another problem -- your Ingredient class does not have a default constructor, and its single parameterized throws a NullReferenceException if the line argument is null. In the absence of a default constructor Json.NET will call the single parameterized constructor, mapping JSON object values to constructor arguments by name. Thus, deserialization throws an exception.

You have a few ways to deal with this:

  1. Add a public default constructor.

  2. Add a private default constructor and mark it with [JsonConstructor]:

    [JsonConstructor]
    Ingredient() { }
    
  3. Add a private default constructor and deserialize with ConstructorHandling.AllowNonPublicDefaultConstructor:

    var settings = new JsonSerializerSettings { ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor };
    var recipe = JsonConvert.DeserializeObject<Recipe>(json, settings);
    
  4. Add an if (line != null) check in the constructor. (Not really recommended. Instead your constructor should explicitly throw an ArgumentNullException.)

Having done this, you will gt JSON that looks like:

{
  "IngredientsWithHeaders": [
    {
      "Header": "BlankHeader",
      "Items": [
        {
          "Quantity": "3",
          "Modifier": null,
          "Unit": "tbsp",
          "IngredientName": "butter",
          "Preparation": null
        }
      ]
    }
  ],
}

Your proposed JSON has an extra level of nesting with

{
"IngredientsWithHeaders": [
    {
        "Group": {
            "Header": "BlankHeader",

This extra "Group" object is unnecessary.

Sign up to request clarification or add additional context in comments.

1 Comment

I just implemented these changes and saw an immediate difference. Thanks for addressing the extra level of nesting - I knew my example of what I wanted wasn't quite optimal, but I wasn't sure exactly what it should look like.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.