Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
212 views
in Technique[技术] by (71.8m points)

c# - Json.net `JsonConstructor` constructor parameter names

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...

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...