Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ClientRuntime] Fixed polymorphic deserialization #3503

Merged
merged 8 commits into from
Aug 24, 2017

Conversation

olydis
Copy link
Contributor

@olydis olydis commented Jul 21, 2017

Issue 1 (see 1st commit for repro): Multi-level inheritance

Happens when <static type of property> != <base type> && <static type of property> != <leaf type> && <dynamic type of property> == <leaf type>
Then <dynamic type of property> != <leaf type> after round trip.

Description

Polymorphic deserialization has broken cases. Consider the following scenario:

class A {}
class B : A {}
class C : B {}

An API method returning an A will call JsonConvert.DeserializeObject<A>(...response body...). The PolymorphicDeserializeJsonConverter we have will successfully deserialize objects of dynamic type A, B and C using the discriminator.
Now, there may be another API method returning B. AutoRest will (reasonably) emit JsonConvert.DeserializeObject<B>(...response body...). Note that the service may return an object of dynamic type B or C. However, PolymorphicDeserializeJsonConverter will not be aware that it still applies so we will always deserialize into B!

Summary

Current behavior: PolymorphicDeserializeJsonConverter only works correctly for JsonConvert.DeserializeObject<T> where T = <root type> (the root type is the type that the discriminator was declared on)
Expected behavior: PolymorphicDeserializeJsonConverter works correctly for JsonConvert.DeserializeObject<T> where T : <root type>, i.e. T is any type in the inheritance hierarchy.

Note that this problem is only exposed for hierarchies of depth at least 2 since otherwise you are dealing with either the root type (which the converter works fine for) or a leaf type (which works fine natively).

Related issue: Azure/autorest#2430

Issue 2 (see 2nd commit for repro): Polymorphic property on base class of polymorphic hierarchy

Happens when <static type of property's declaring class> == <base type> && <dynamic type of property's declaring class> == <base type> && <dynamic type of property> != <static type of property>
Then <dynamic type of property> == <static type of property> after round trip.

Description

Consider the following scenario:

class A { public A Friend { get; set; } }
class B : A {}

Then

new A { Friend = new A() } // round trips
new A { Friend = new B() } // fails round trip ("Friend" will have dyn. type A!)
new B { Friend = new A() } // round trips
new B { Friend = new B() } // round trips

Summary

When polymorphic types are deserialized and the base type is detected (above: A), the object is deserialized with the default deserializer, i.e. without passing along the polymorphic deserializer.
As a result, any properties (transitively) of the object will be naively deserialized into their static types.


This checklist is used to make sure that common guidelines for a pull request are followed.

General Guidelines

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, see this page.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

@olydis olydis force-pushed the polymorphic-serialization branch 5 times, most recently from 7e53f43 to 787d060 Compare July 21, 2017 21:52
@olydis
Copy link
Contributor Author

olydis commented Jul 27, 2017

@shahabhijeet any clue why CI no longer terminates? It actually is hanging up on the CR tests, so it must be my change. Could it be the fact that I'm using reflection? WTF.

@shahabhijeet
Copy link
Member

@olydis looking at the logs it is not clear at all as to why the test process was terminated.

var json = JsonConvert.SerializeObject(instance, SerializeJsonConverter);
return JsonConvert.DeserializeObject<T>(json, DeserializeJsonConverter);
}
private static void AssertRoundTrips<T>(T instance) where T : Model
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olydis do we have any negative tests, where it fails to convert if we reverse the order of inheritance hierarchy? If not we should add those.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will add

@olydis
Copy link
Contributor Author

olydis commented Jul 27, 2017

@shahabhijeet well, it says timeout, but I don't get how my test times out on both Linux and Windows... will try to repro locally, but it definitely ran through on my machine before 😞

@olydis olydis force-pushed the polymorphic-serialization branch 3 times, most recently from 9bc82ab to 06c2225 Compare July 28, 2017 16:22
@olydis olydis changed the title [ClientRuntime] Fixed polymorphic deserialization in case of multi-level inheritance (WIP) [ClientRuntime] Fixed polymorphic deserialization (WIP) Jul 28, 2017
@olydis
Copy link
Contributor Author

olydis commented Jul 28, 2017

@shahabhijeet investigating the non-termination I exposed an even more fundamental issue... my fix will have to be more sophisticated 😉

@olydis olydis force-pushed the polymorphic-serialization branch 2 times, most recently from f0423e8 to 2008a09 Compare July 28, 2017 20:47
@olydis olydis changed the title [ClientRuntime] Fixed polymorphic deserialization (WIP) [ClientRuntime] Fixed polymorphic deserialization Jul 28, 2017
@olydis olydis requested a review from markcowl July 28, 2017 21:50
@olydis
Copy link
Contributor Author

olydis commented Jul 28, 2017

@markcowl @shahabhijeet Note that I had to essentially rewrite the deserializer from scratch, it was very fundamentally broken, I'll gladly share details.

In short: Calling ToObject reevaluates JSON converters. We called that from inside a JSON converter. That quickly leads to infinite recursion and an SO unless you "trick around" a bit to prevent recusrsion.
Trick 1: CanConvert only returns true if the type asked for is the root of the hierarchy. After one iteration of the converter, the type will be more narrow. => termination since CanConvert now returns false (but this causes issue 1)
Trick 2: If the discriminator actually points to the root type of the hierarchy, the object is deserialized without further application of any converters to the entire object tree. => termination since... no more converters (but this causes issue 2)

My current approach explicitly traverses down into the properties before calling out to Newtonsoft.JSON again.

@olydis olydis force-pushed the polymorphic-serialization branch 2 times, most recently from bbf65c2 to fbcf230 Compare July 31, 2017 22:25
@olydis olydis force-pushed the polymorphic-serialization branch 2 times, most recently from 886bbed to 5799ae4 Compare August 7, 2017 22:28
@olydis
Copy link
Contributor Author

olydis commented Aug 9, 2017

@shahabhijeet MachineLearning tests with the weird boolean toggle problem (that I really couldn't connect to polymorphic deserialization) pass now! All I did was rebase onto your most recent changes in psSdkJson6 and it worked out of the box. I haven't committed my version bumps, that may simplify combining our changes for the 2.4.0 release.

@shahabhijeet shahabhijeet merged commit 7c47282 into Azure:psSdkJson6 Aug 24, 2017
JasonYang-MSFT pushed a commit to JasonYang-MSFT/azure-sdk-for-net that referenced this pull request Dec 8, 2017
* reproing test (deep hiararchy)

* reproing test (base class property)

* fix

* bold move

* tiny improvement to generate.cmd (just making sure you get the same experience even when calling one of the RPs scripts)

* case insensitive property lookup (I guess we're already down that rabbit hole)

* tiny improvement to generate.cmd (mention commit IDs, clarifications)

* tiny improvement to generateMetadata.cmd (handle commit IDs)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants