When Json.NET invokes a parameterized constructor, it matches JSON properties to constructor arguments by name, using an ordinal case-ignoring match. However, for JSON properties that also correspond to type members, which name does it use – the member name, or the override type member name specified by JsonPropertyAttribute.PropertyName
?
It appears you are hoping it matches on both, since your argument naming conventions are inconsistent:
-
The constructor argument
production_countries
matches the overridden property name:[JsonProperty("production_countries")] public IList<IProductionCountry> ProductionCountries { get; set; }
-
The constructor argument
IList<SpokenLanguage> SpokenLanguages
matches the reflected name rather than the overridden property name:[JsonProperty("spoken_languages")] public IList<ISpokenLanguage> SpokenLanguages { get; set; }
-
IList<SysType> SysTypes
matches neither (is this a typo in the question?)
However, what matters is the property name in the JSON file itself and the constructor argument name as shown in JsonSerializerInternalReader.ResolvePropertyAndCreatorValues()
. A simplified version of the algorithm is as follows:
- The property name is read from the JSON file.
- A closest match constructor argument is found (if any).
- A closest match member name is found (if any).
- If the JSON property matched a constructor argument, deserialize to that type and pass into the constructor,
- But if not, deserialize to the appropriate member type and set the member value after construction.
(The implementation becomes complex when a JSON property matches both and developers expect that, for instance, [JsonProperty(Required = Required.Always)]
added to the member should be respected when set in the constructor.)
Thus the constructor argument production_countries
will match a value named "production_countries"
in the JSON, while the constructor argument SpokenLanguages
will not match a JSON value named "spoken_languages"
.
So, how to deserialize your type successfully? Firstly, you could mark the constructor parameters with [JsonProperty(overrideName)]
to override the constructor name used during deserialization:
public partial class AClass : ISomeBase
{
public AClass() { }
[JsonConstructor]
public AClass([JsonProperty("Genres")] IList<SysType> SysTypes, IList<ProductionCountry> production_countries, [JsonProperty("spoken_languages")] IList<SpokenLanguage> SpokenLanguages)
{
this.Genres = SysTypes == null ? null : SysTypes.Cast<IGenre>().ToList();
this.ProductionCountries = production_countries == null ? null : production_countries.Cast<IProductionCountry>().ToList();
this.SpokenLanguages = SpokenLanguages == null ? null : SpokenLanguages.Cast<ISpokenLanguage>().ToList();
}
Secondly, since you seem to be using the constructor to deserialize items in collections containing interfaces as concrete objects, you could consider using a single generic converter based on CustomCreationConverter
as an ItemConverter
:
public partial class AClass : ISomeBase
{
public AClass() { }
public int Id { get; set; }
[JsonProperty(ItemConverterType = typeof(CustomCreationConverter<IGenre, SysType>))]
public IList<IGenre> Genres { get; set; }
[JsonProperty("production_countries", ItemConverterType = typeof(CustomCreationConverter<IProductionCountry, ProductionCountry>))]
public IList<IProductionCountry> ProductionCountries { get; set; }
[JsonProperty("spoken_languages", ItemConverterType = typeof(CustomCreationConverter<ISpokenLanguage, SpokenLanguage>))]
public IList<ISpokenLanguage> SpokenLanguages { get; set; }
}
public class CustomCreationConverter<T, TSerialized> : CustomCreationConverter<T> where TSerialized : T, new()
{
public override T Create(Type objectType)
{
return new TSerialized();
}
}
Example fiddle showing both options.