question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Invalid deserialisation order for required fields

See original GitHub issue

Source JSON

{}

Destination type

public class SampleType
{
	[JsonProperty(Required = Required.Always)]
	public int SampleField { get; }

	[JsonConstructor]
	public SampleType(int sampleField)
	{
		if (sampleField == 0) {
			throw new Exception("Should not be zero");
		}
		SampleField = sampleField;
	}
}

Expected behavior

Deserialization exception is thrown:

Required property ‘SampleField’ not found in JSON. Path ‘’, line 1, position 2.

Actual behavior

User exception is thrown:

Should not be zero

Steps to reproduce

JsonConvert.DeserializeObject<SampleType>("{}");

Issue Analytics

  • State:open
  • Created 4 years ago
  • Comments:8

github_iconTop GitHub Comments

3reactions
dbc2commented, Jul 19, 2019

Json.NET seems to be working as designed here. Two points:

  1. When deserializing using a parameterized constructor, Json.NET will automatically provide a default value for any constructor arguments without matching JSON properties.

    This is described in, e.g. this answer to Newtonsoft json deserialise missing int values as nulls instead of zero and this answer to How does JSON deserialization in C# work.

  2. When the c# data model has required properties and one or more are missing, then a JsonSerializationException is thrown after deserialization is complete.

    When deserializing an object with a default constructor this falls out naturally from Json.NET’s streaming approach to deserialization. First the object is constructed. Next Json.NET streams through the JSON, deserializing and populating each property and tracking which ones were found. Finally, if a required property was not encountered, an exception is thrown at the end of the process.

    It seems as though Newtonsoft made a conscious decision to preserve this order when deserializing objects with parameterized constructors. They did this despite the fact that it would have been possible to throw the exception at the beginning of deserialization, since the JSON gets preloaded and deserialized into property and argument values before the object is constructed.

    To confirm this, see JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters(). Checking for missing properties is done in the call to EndProcessProperty() which happens is towards the end of the algorithm, just before calling finally OnDeserialized.

If I modify your type as follows, so that validation of sampleField is done in an OnDeserialized method and a factory method that checks for appropriate argument values replaces the public parameterized constructor:

public class SampleType
{
    int sampleField;

    [JsonProperty(Required = Required.Always)]
    public int SampleField { get { return sampleField; } }

    public static SampleType Create(int sampleField)
    {
        Validate(sampleField);
        return new SampleType(sampleField);
    }

    [JsonConstructor]
    internal SampleType(int sampleField)
    {
        this.sampleField = sampleField;
    }

    [System.Runtime.Serialization.OnDeserialized]
    void OnDeserializedMethod(System.Runtime.Serialization.StreamingContext context)
    {
        Validate(SampleField);
    }

    static void Validate(int sampleField)
    {
        if (sampleField == 0)
        {
            throw new Exception("Should not be zero");
        }
    }
}

Then a JsonSerializationException is thrown as expected, rather than the custom exception. Demo fiddle here.

I cannot, however, find anything in the official documentation that explains exactly how JSON properties are matched to constructor arguments, what happens when a constructor argument is missing, how and whether [JsonProperty] attributes applied to .Net properties affect the deserialization of similarly named constructor arguments, or when missing property validation occurrs. Those are all topics that Newtonsoft might want to clarify in their documentation.

1reaction
bartelinkcommented, Jul 9, 2019

Apologies - seems I scanned too fast - I’m surprised it entered the constructor as you’re saying; I agree there is a case to be understood. But, the truth re these edge cases will definitely be in the tests…

Read more comments on GitHub >

github_iconTop Results From Across the Web

Serialize/deserialize objects - order of fields matters?
The classes that I try to serialize/deserialize do not have order-attributes placed on fields/properties. Yet one of my fields always gets ...
Read more >
JSON error when deserializing data
Hello all-. I'm getting the following when trying to deserialize a previously serialized object: System.JSONException: Invalid format: ...
Read more >
Jackson – Decide What Fields Get Serialized/Deserialized
The simplest way to make sure a field is both serializable and deserializable is to make it public. Let's declare a simple class...
Read more >
Skip invalid elements in a sequence with Serde deserialization
Using Serde, I would like to Deserialize a sequence of elements by keeping the valid elements and skip the invalid ones.
Read more >
Migrate from Newtonsoft.Json to System.Text.Json - .NET
Newtonsoft.Json can serialize and deserialize fields as well as properties. In System.Text.Json, use the JsonSerializerOptions.IncludeFields ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found