You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I've found myself needing to use the JsonSerializerSettings.Error property to capture certain types of exceptions that can occur during deserialization. That setting contains an error handler delegate which is passed an ErrorEventArgs object. I am relying on the args.ErrorContext.Path property to contain the full path to the value which caused a given exception. This works fine for most types of objects, but I found that the JsonSubtypes library seems to break the paths of any errors that occur within the types of objects it is configured to convert. I've provided a handful of examples below that show how errors behave normally with Newtonsoft.Json versus how they behave when using JsonSubtypes. See this documentation for how errors are meant to be handled in Newtonsoft.Json.
Example 2: Error that doesn't involve JsonSubtypes.
{"Property1": {"Value": "not a bool"}}
Example 3: Error from an object that uses a discriminator.
{"Property2": {"$kind": "a","Value": "not a float"}}
Example 4: Error from an object that doesn't use a discriminator.
{"Property3": {"Value": "not an int"}}
Expected behavior
Example 1
No errors received by JsonSerializerSettings.Error and no exception thrown by serializer.Deserialize.
Example 2
JsonSerializerSettings.Error receives a single error event for which args.CurrentObject == args.ErrorContext.OriginalObject, and that event has a path of "Property1.Value". The exception thrown by serializer.Deserialize has the same path.
Example 3
JsonSerializerSettings.Error receives a single error event for which args.CurrentObject == args.ErrorContext.OriginalObject, and that event has a path of "Property2.Value". The exception thrown by serializer.Deserialize has the same path.
Example 4
JsonSerializerSettings.Error receives a single error event for which args.CurrentObject == args.ErrorContext.OriginalObject, and that event has a path of "Property3.Value". The exception thrown by serializer.Deserialize has the same path.
Actual behavior
Examples 1 and 2
Both of these behave as expected. I've just included them as baseline examples.
Example 3
JsonSerializerSettings.Error receives two error events for which args.CurrentObject == args.ErrorContext.OriginalObject. The first has a path of "Value", while the second has a path of "Property2". The exception thrown by serializer.Deserialize just has the path "Value".
Example 4
JsonSerializerSettings.Error receives two error events for which args.CurrentObject == args.ErrorContext.OriginalObject. The first has a path of "Value", while the second has a path of "Property3". The exception thrown by serializer.Deserialize just has the path "Value".
Steps to reproduce
Full code example
usingJsonSubTypes;usingNewtonsoft.Json;namespaceJsonSubtypesBug;internalclassProgram{staticvoidMain(){// Expected result: No errors/exceptions// Behaves as expected.Console.WriteLine("=== TEST 1 ===");PrintDeserializationErrors(""" { "Property1": { "Value": true }, "Property2": { "$kind": "a", "Value": 3.14 }, "Property3": { "Value": 42 } } """);// Expected result: Error at "Property1.Value", exception from the same path.// Behaves as expected.Console.WriteLine("\n=== TEST 2 ===");PrintDeserializationErrors(""" { "Property1": { "Value": "not a bool" } } """);// Expected result: Error at "Property2.Value", exception from the same path.// Actual result: Error at "Value" and at "Property2", exception from "Value".Console.WriteLine("\n=== TEST 3 ===");PrintDeserializationErrors(""" { "Property2": { "$kind": "a", "Value": "not a float" } } """);// Expected result: Error at "Property3.Value", exception from the same path.// Actual result: Error at "Value" and at "Property3", exception from "Value".Console.WriteLine("\n=== TEST 4 ===");PrintDeserializationErrors(""" { "Property3": { "Value": "not an int" } } """);}staticvoidPrintDeserializationErrors(stringjson){JsonSerializerSettingssettings=new(){Converters={JsonSubtypesConverterBuilder.Of<BaseClass>("$kind").RegisterSubtype<SubClassA>("a").RegisterSubtype<SubClassB>("b").Build()},Error=HandleError};varserializer=JsonSerializer.CreateDefault(settings);try{usingvarjsonReader=newJsonTextReader(newStringReader(json));serializer.Deserialize<RootObject>(jsonReader);Console.WriteLine("No exception was thrown.");}catch(JsonReaderExceptionex){Console.WriteLine($"Caught exception that originated from '{ex.Path}': {ex.Message}");}voidHandleError(object?sender,Newtonsoft.Json.Serialization.ErrorEventArgsargs){if(args.CurrentObject==args.ErrorContext.OriginalObject){Console.WriteLine($"Encountered error at '{args.ErrorContext.Path}': {args.ErrorContext.Error.Message}");}}}}// Types to deserializeinternalclassRootObject{publicNestedObject?Property1{get;set;}publicBaseClass?Property2{get;set;}publicSubClassB?Property3{get;set;}}internalclassNestedObject{publicboolValue{get;set;}}internalabstractclassBaseClass{}internalclassSubClassA:BaseClass{publicfloatValue{get;set;}}internalclassSubClassB:BaseClass{publicintValue{get;set;}}
Possible Causes
I'm not super familiar with the internals of this library, but I did poke around a little bit while diagnosing this issue and came across a couple details that might be of help.
First of all, it seems that the JsonReader object is responsible for maintaining the current path being read. When deserializing a polymorphic type, JsonSubtypes creates a new JsonReader specifically for the JSON object being read. However, the way it does this doesn't initialize the reader with the current path. Instead, the reader starts out with an empty path. See this method in the source code.
I used my debugger to force the reader to instead be created using new JTokenReader(jToken, reader.Path), but this led to a new issue. It seems that to avoid getting stuck in an infinite recursion loop, JsonSubtypes relies on the reader path being empty as a means of indicating that it has already begun reading a given object. See this line of source code.
Using my debugger to forcefully break out of that loop (even when the path isn't empty), I was able to get error paths to appear as they should. The error still appears to occur twice (i.e., there are two events for which args.CurrentObject == args.ErrorContext.OriginalObject), but the path the error holds is at least correct.
The text was updated successfully, but these errors were encountered:
I've found myself needing to use the
JsonSerializerSettings.Error
property to capture certain types of exceptions that can occur during deserialization. That setting contains an error handler delegate which is passed anErrorEventArgs
object. I am relying on theargs.ErrorContext.Path
property to contain the full path to the value which caused a given exception. This works fine for most types of objects, but I found that the JsonSubtypes library seems to break the paths of any errors that occur within the types of objects it is configured to convert. I've provided a handful of examples below that show how errors behave normally with Newtonsoft.Json versus how they behave when using JsonSubtypes. See this documentation for how errors are meant to be handled in Newtonsoft.Json.Source/destination types
Source/destination JSON
Example 1: No errors.
Example 2: Error that doesn't involve JsonSubtypes.
Example 3: Error from an object that uses a discriminator.
Example 4: Error from an object that doesn't use a discriminator.
Expected behavior
Example 1
No errors received by
JsonSerializerSettings.Error
and no exception thrown byserializer.Deserialize
.Example 2
JsonSerializerSettings.Error
receives a single error event for whichargs.CurrentObject == args.ErrorContext.OriginalObject
, and that event has a path of"Property1.Value"
. The exception thrown byserializer.Deserialize
has the same path.Example 3
JsonSerializerSettings.Error
receives a single error event for whichargs.CurrentObject == args.ErrorContext.OriginalObject
, and that event has a path of"Property2.Value"
. The exception thrown byserializer.Deserialize
has the same path.Example 4
JsonSerializerSettings.Error
receives a single error event for whichargs.CurrentObject == args.ErrorContext.OriginalObject
, and that event has a path of"Property3.Value"
. The exception thrown byserializer.Deserialize
has the same path.Actual behavior
Examples 1 and 2
Both of these behave as expected. I've just included them as baseline examples.
Example 3
JsonSerializerSettings.Error
receives two error events for whichargs.CurrentObject == args.ErrorContext.OriginalObject
. The first has a path of"Value"
, while the second has a path of"Property2"
. The exception thrown byserializer.Deserialize
just has the path"Value"
.Example 4
JsonSerializerSettings.Error
receives two error events for whichargs.CurrentObject == args.ErrorContext.OriginalObject
. The first has a path of"Value"
, while the second has a path of"Property3"
. The exception thrown byserializer.Deserialize
just has the path"Value"
.Steps to reproduce
Full code example
Possible Causes
I'm not super familiar with the internals of this library, but I did poke around a little bit while diagnosing this issue and came across a couple details that might be of help.
First of all, it seems that the
JsonReader
object is responsible for maintaining the current path being read. When deserializing a polymorphic type, JsonSubtypes creates a newJsonReader
specifically for the JSON object being read. However, the way it does this doesn't initialize the reader with the current path. Instead, the reader starts out with an empty path. See this method in the source code.I used my debugger to force the reader to instead be created using
new JTokenReader(jToken, reader.Path)
, but this led to a new issue. It seems that to avoid getting stuck in an infinite recursion loop, JsonSubtypes relies on the reader path being empty as a means of indicating that it has already begun reading a given object. See this line of source code.Using my debugger to forcefully break out of that loop (even when the path isn't empty), I was able to get error paths to appear as they should. The error still appears to occur twice (i.e., there are two events for which
args.CurrentObject == args.ErrorContext.OriginalObject
), but the path the error holds is at least correct.The text was updated successfully, but these errors were encountered: