Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Json.net `JsonConstructor` constructor parameter names

Tags:

c#

json.net

When using a specific .ctor via JsonConstructor for deserializing IList<ISomeInterface> properties, the parameter names must match the original Json names and the JsonProperty mapping on those properties are not used.

Example:

SpokenLanguages parameter is always null since it does not match spoken_languages, but there is a JsonProperty mapping it:

public partial class AClass : ISomeBase
{
    public AClass() { }

    [JsonConstructor]
    public AClass(IList<SysType> SysTypes, IList<ProductionCountry> production_countries, IList<SpokenLanguage> SpokenLanguages)
    {
        this.Genres = SysTypes?.ToList<IGenre>();
        this.ProductionCountries = production_countries?.ToList<IProductionCountry>();
        this.SpokenLanguages = SpokenLanguages?.ToList<ISpokenLanguage>();
    }

    public int Id { get; set; }
    public IList<IGenre> Genres { get; set; }
    [JsonProperty("production_countries")]
    public IList<IProductionCountry> ProductionCountries { get; set; }
    [JsonProperty("spoken_languages")]
    public IList<ISpokenLanguage> SpokenLanguages { get; set; }
}

Is this just a "limitation" of how Json.Net calls the constructor or is there something I am missing.

FYI: I am code generating all this via Rosyln and am not looking at generating a JsonConverter for each type for this...

like image 951
SushiHangover Avatar asked Mar 26 '17 18:03

SushiHangover


1 Answers

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.

like image 137
dbc Avatar answered Oct 29 '22 13:10

dbc