Json.net `JsonConstructor` constructor parameter names

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:

  1. The property name is read from the JSON file.
  2. A closest match constructor argument is found (if any).
  3. A closest match member name is found (if any).
  4. If the JSON property matched a constructor argument, deserialize to that type and pass into the constructor,
  5. 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.

Leave a Comment

tech