问题
I'm deserializing a bunch of C# readonly structures (which have their constructors marked by [JsonConstructor]
), and I'm trying to fail early if any JSON that I receive is malformed.
Unfortunately, if there is a naming discrepancy between the constructor parameter and the input JSON, the parameter just gets assigned a default value. Is there a way that I could get an exception instead, so these defaults don't accidentally "pollute" the rest of my business logic? I have tried playing with various JsonSerializerSettings
but to no avail.
Simplified example:
public readonly struct Foo {
[JsonConstructor]
public Foo(long wrong) {
FooField = wrong;
}
public readonly long FooField;
}
public void JsonConstructorParameterTest() {
// The Foo constructor parameter name ("wrong") doesn't match the JSON property name ("FooField").
var foo = JsonConvert.DeserializeObject<Foo>("{\"FooField\":42}");
// The foo.FooField is now 0.
// How can we cause the above to throw an exception instead of just assigning 0 to Foo.FooField?
}
The above can be fixed by renaming wrong
into fooField
, but I'd like to know that before 0 has already been committed to my database.
回答1:
The contract resolver from this answer to JSON.net should not use default values for constructor parameters, should use default for properties almost does what you want, however it has a noted restriction:
This only works if there is a corresponding property. There doesn't appear to be a straightforward way to mark a constructor parameter with no corresponding property as required.
Since marking an "unmatched" constructor parameter as required doesn't seem to work (demo fiddle #1 here) you can modify the contract resolver from that answer to throw an exception during contract construction if an unmatched constructor parameter is found.
The following contract resolver does this:
public class ConstructorParametersRequiredContractResolver : DefaultContractResolver
{
protected override JsonProperty CreatePropertyFromConstructorParameter(JsonProperty matchingMemberProperty, ParameterInfo parameterInfo)
{
// All constructor parameters are required to have some matching member.
if (matchingMemberProperty == null)
throw new JsonSerializationException(string.Format("No matching member for constructor parameter \"{0}\" of type \"{1}\".", parameterInfo, parameterInfo.Member.DeclaringType));
var property = base.CreatePropertyFromConstructorParameter(matchingMemberProperty, parameterInfo);
if (property != null && matchingMemberProperty != null)
{
if (!matchingMemberProperty.IsRequiredSpecified) // If the member is already explicitly marked with some Required attribute, don't override it.
{
Required required;
if (matchingMemberProperty.PropertyType != null && (matchingMemberProperty.PropertyType.IsValueType && Nullable.GetUnderlyingType(matchingMemberProperty.PropertyType) == null))
{
required = Required.Always;
}
else
{
required = Required.AllowNull;
}
// It turns out to be necessary to mark the original matchingMemberProperty as required.
property.Required = matchingMemberProperty.Required = required;
}
}
return property;
}
}
To use it, construct the resolver:
static IContractResolver resolver = new ConstructorParametersRequiredContractResolver();
And unit test as follows:
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
};
JsonConvert.DeserializeObject<Foo>("{\"FooField\":42}", settings);
Note you may want to cache and reuse the contract resolver for best performance.
Demo fiddle #2 here.
来源:https://stackoverflow.com/questions/63898984/how-to-throw-an-exception-when-jsonconstructor-parameter-name-doesnt-match-json