Reputation: 69
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>
?
Upvotes: 1
Views: 1727
Reputation: 116532
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:
Write your own custom JsonConverter
. You can determine the generic arguments using reflection along the lines of Json.Net returns Empty Brackets.
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:
Add a public default constructor.
Add a private default constructor and mark it with [JsonConstructor]
:
[JsonConstructor]
Ingredient() { }
Add a private default constructor and deserialize with ConstructorHandling.AllowNonPublicDefaultConstructor
:
var settings = new JsonSerializerSettings { ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor };
var recipe = JsonConvert.DeserializeObject<Recipe>(json, settings);
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.
Upvotes: 2