-
Notifications
You must be signed in to change notification settings - Fork 218
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
C# Model - oneOf of list and object is not deserialized correctly #3976
Comments
Hi @Irame Does your Item object return a property that maps to sub-types somehow (e.g. if item is vehicle, that property would return car, truck, train...) ? |
Hi thanks for the quick response. Unfortunately, there isn't a specific property with a fixed value that indicates whether it's a single object or something else. The specific API that has this problem is the UPS API, and you can find their OpenAPI files here: https://github.com/UPS-API/api-documentation. One example would be within the Shipping.json file, the schema for |
We might be able to make your case work better by returning null when the JSON we're reading is not an array here But this would still require the manual edit you've made (and now the empty lists would work as long as you use a null propagation operator before the count). The reason why I'm reluctant to re-order the generated code differently is because while this would improve your use case, it'd make most cases slower or might even break them (in case you have a truly discriminated union, you might end up with one property initialized when it shouldn't). Does that make sense? |
Returning null here seems like a good idea, but I'm not sure how rearranging the checks could cause issues. From what I understand, if it's a list, it wouldn't have a discriminator value, so it's safe to treat it as a list. Otherwise, we can check the discriminator value. Am I missing anything here? |
Let's say we have a Personable exclusive union type that's implemented by Employee/Customer/Shareholder. public static Personable CreateFromDiscriminatorValue(IParseNode parseNode) {
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
var mappingValue = parseNode.GetChildNode("personType")?.GetStringValue();
var result = new Personable();
if("Customer".Equals(mappingValue, StringComparison.OrdinalIgnoreCase)) {
result.Customer = new Customer();
}
else if("Employee".Equals(mappingValue, StringComparison.OrdinalIgnoreCase)) {
result.Employee = new Employee();
}
else if("Shareholder".Equals(mappingValue, StringComparison.OrdinalIgnoreCase)) {
result.Employee = new Shareholder();
}
return result;
} This implies that if the API starts randomly returning a new "DomesticPartner" type that's not in the description, we won't have any side effect here, none of the properties will be initialized, and the client application will "know" something is wrong. (description needs to be updated, client refreshed, and that case handled....) Now, in the edit you've made, that I'm copying here for context public static Result_OneOrManyItems CreateFromDiscriminatorValue(IParseNode parseNode) {
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
var result = new Result_OneOrManyItems();
var list = parseNode.GetCollectionOfObjectValues<ApiSdk.Models.Item>(ApiSdk.Models.Item.CreateFromDiscriminatorValue)?.ToList() as List<ApiSdk.Models.Item>;
if((list.Count ?? 0) == 0) {
result.ResultOneOrManyItemsItem = new ApiSdk.Models.Item();
}
else {
result.Item = list;
}
return result;
} Notice how the code went from checking the discriminator value to simply defaulting back to the single value? If we transposed that to my example this raises multiple questions:
Assuming we change the deserialization code to return null when the result is not an array, and keep the originally generated code in your case. Here is what will happen:
With the change in deserialization behaviour, and this edit, everything should work as expected, without impacting negatively other scenarios. public static Result_OneOrManyItems CreateFromDiscriminatorValue(IParseNode parseNode) {
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
var result = new Result_OneOrManyItems();
if(parseNode.GetCollectionOfObjectValues<ApiSdk.Models.Item>(ApiSdk.Models.Item.CreateFromDiscriminatorValue)?.ToList() is List<ApiSdk.Models.Item> itemValue) {
result.Item = itemValue;
} else if (parseNode.GetObjectValue(ApiSdk.Models.Item.CreateFromDiscriminatorValue) is {} itemValue) {
result.ResultOneOrManyItemsItem = itemValue;
}
return result;
} The only side effect of doing that is it might lead to double deserialization in the case of a single object (not in a collection) which will slightly impact performance. With that context in mind, would you be willing to contribute to the serialization library? |
Hi I took a look at the serialization library and noticed that in order to make this change the return type of I created two draft pull requests. I hope this is ok and I didn't skip any wanted discussion with this. |
yes, I've seen the draft pull requests, thanks for starting that. (python might need fixing as well) What comforts me in that change is the fact the generated code won't need to change And the fact the current implementation can lead to nasty bugs without the backing store:
Before we proceed I'd like the opinion of @andrueastman on the topic, especially on the abstractions PR. I think for the static methods it's fine since we only introduced them a couple of weeks ago. What I'm interested in is the change for the parse node interface. (see links in the history above) |
Apparently the dotnet team ran into a similar issue and they decided to roll out the changes within the same major version |
gentle reminder @andrueastman on this one. It seems we forgot about it. Sorry about that @Irame |
gentle reminder for @andrueastman to provide his input |
I want to consume an API that has the following construct for properties (
OneOrManyItems
in this example) at quite a lot of places:Its basically a property (
OneOrManyItems
) that can be a list of objects or just one object (both with the same typeItem
).When I now generate a client using this command:
kiota generate -d desciption.json -l CSharp
.I get a model for the result that does not parse the json response correctly. It only parses it correctly for the case that it is a list of items not a single item.
The
Result_OneOrManyItems
class inside theResult
looks something like this:When deserialized the
ResultOneOrManyItemsItem
property is always null and theItem
is always non null but only contains something, if the parsed json is an array.The problem seems to me that the
CreateFromDiscriminatorValue
function only looks at the discriminator value even if there is none and not at the type of the json that is being parsed.The solution I found for my usecase looks like this:
This is far from ideal because it does not allow empty lists and does not use the real underling type. I also have to touch a lot of generated code for this solution which is not convenient and error prone.
This Issue is probably similar to #2338 but it wasn't 100% my use case, hence I created a new issue.
I also saw a similar API description in this issue #3963 but the problem was a different one.
If this is not fixable in the short term is there a better workaround than adjusting all implementations of
CreateFromDiscriminatorValue
by hand?The text was updated successfully, but these errors were encountered: