-
Notifications
You must be signed in to change notification settings - Fork 217
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
Offer helper to deserialize API response to an entity #3406
Comments
Hi @waldekmastykarz , Assigning to @sebastienlevert to refine the developer experience here. While a static method on the JsonSerializationWriter is easy, we need to think about a couple of things:
|
Just capturing notes from our meeting now An alternative could be to do binding instead of requesting the factory: var fruit = new Fruit(); // user creates the instance
KiotaMagicMapper.Bind(fruit, "application/json", "{\"color\":\"red\",\"name\":\"apple\"}"); // standard deserialization code is being used
// would call ===>>> JsonParseNode.GetObjectValue(_ => instance); we don't need to ask for the factory since we already have the instance This works well for objects, but starts breaking down for collections |
What if we made it event easier? Each request builder is tied to an entity, right? What if we'd let developers pass either an entity or an object and if it's just an object (like what you get when you deserialize a JSON string), we create a new instance of the entity and bind the object to it? That way, developers don't need to learn any special concepts to use Kiota. |
In most languages the serialization and deserialization information is tied to the object itself (class implements an interface). That's because kiota leverages auto serialization to avoid relying on reflection. It's a bit less flexible but it's faster. |
Right, but if we pass the entity type as part of the serialization then we could create a new instance of it and then overlay the regular object on top of it, right, say: Kiota.Magic.Deserialize<Fruit>("application/json", "{{json string goes here}}") and then inside Deserialize we create a new instance of Fruit because we've got a guard that it needs to be Thinking of it, since Graph is all about JSON, we might skip the content type to simplify it further. I know that in that sense Kiota is generic and works with any API and content type, but specifically for Graph, we could layer abstraction over it, to make it easier for devs. |
This only works for dotnet, we've generally avoiding designing solutions that only work for a single language. |
This is the most challenging (and most important) aspect of these designs. It needs to support existing (and even future) languages. This would not be a language-specific implementation. I understand the concern and this might be something to be simplified on every language we support Graph (building another wrapper simplifying the experience). I still feel we should fix it upstream and make it as developer-friendly as possible before adapting the approach to Graph, for instance. Also in the initial proposal from @baywet we are missing collection support. What would be missing to make this support scalar and collections? |
I understand that and appreciate that it's hard. Still, it shouldn't come at the cost of developer experience.
The problem with this approach is settling for the lowest common denominator leading to a subpar experience for all developers, rather than making use of language-specific features that enrich developer experience. I wonder how many developers use several different languages and seek consistency across all of them vs. use one language and would like to fully benefit of its features. |
To clarify the point: I don't think we're saying not to take advantage of language specific feature when we can so we can deliver the best of class experience. We already do this (e.g. indexers in dotnet). |
My gut feeling tells me, that we should make the best use of language features and native patterns at the cost of inconsistency across languages. If we chose this route, then we'd need to investigate each language and its capabilities, which means more work and deviation but ideally a significantly better developer experience. |
Part of the reason why we're not able to come up with a simple solution here is because we're not scoping/framing the problem well enough. I'm going to try to attempt that. We want people to be able to easily (de)serialize models to/from a persistent storage. One of the keywords here is models ([collection of]? enums or classes/interfaces), which excludes [collections of]? primitives, for which they can use their favorite library to do so. If we focus on object models for a second, the auto-serialization infrastructure does multiple things for us:
Now that we have a better understanding of what the infrastructure does for us today, let's add a couple of requirements:
With all that in mind, here is my suggestion: binding is now going to work for TypeScript specifically. // Deserializing an object
Kiota.Magic.Deserialize("application/json", "{{json string goes here}}", Fruit.CreateFromDiscriminatorValue);
// Deserializing a collection of object
Kiota.Magic.DeserializeCollection("application/json", "{{json string goes here}}", Fruit.CreateFromDiscriminatorValue);
// Deserializing an enum value
Kiota.Magic.DeserializeEnum("application/json", "{{json string goes here}}", FruitKind.Parse);
// Deserializing a collection of enum values
Kiota.Magic.DeserializeEnumCollection("application/json", "{{json string goes here}}", FruitKind.Parse);
//Serializing an object
Kiota.Magic.Serialize("application/json", streamReference, myFruit);
//Serializing an object collection
Kiota.Magic.SerializeCollection("application/json", streamReference, myFruits);
//Serializing a enum value
Kiota.Magic.SerializeEnum("application/json", streamReference, myFruitKind);
//Serializing a collection of enum values
Kiota.Magic.SerializeEnumValues("application/json", streamReference, myFruitKinds);
Thoughts? |
This is a great investigation @baywet! I think // Deserializing an object
Kiota.Serialization.Deserialize("application/json", "{{json string goes here}}", Fruit.CreateFromDiscriminatorValue);
// Deserializing a collection of object
Kiota.Serialization.DeserializeCollection("application/json", "{{json string goes here}}", Fruit.CreateFromDiscriminatorValue);
// Deserializing an enum value
Kiota.Serialization.DeserializeEnum("application/json", "{{json string goes here}}", FruitKind.Parse);
// Deserializing a collection of enum values
Kiota.Serialization.DeserializeEnumCollection("application/json", "{{json string goes here}}", FruitKind.Parse);
//Serializing an object
Kiota.Serialization.Serialize("application/json", streamReference, myFruit);
//Serializing an object collection
Kiota.Serialization.SerializeCollection("application/json", streamReference, myFruits);
//Serializing a enum value
Kiota.Serialization.SerializeEnum("application/json", streamReference, myFruitKind);
//Serializing a collection of enum values
Kiota.Serialization.SerializeEnumValues("application/json", streamReference, myFruitKinds); Also to confirm with your reasonning, where you put To your questions:
|
To the other formats: yes, this is why we ask for the mime type as well. This way we could rely on the infrastructure to parse or serialize to any format. Great, I think we have a plan. I'd like @waldekmastykarz 's opinion on the experience here as well in case we missed anything. |
Being able to serialize/deserialize using the Kiota infrastructure, at least from/to Json in a convenient way is truly desirable. I'll try to have a look at the details of this proposal tomorrow. |
|
|
|
|
I numbered them well, but them markedown decided that a list with only 1 and 3 was not going to make it!
|
Thanks everyone for the great feedback.
Granted that SerializationHelpers will become KiotaSerialization and JsonSerializationHelpers will become KiotaJsonSerialization, what do you think about the method names? I had to add the AsStream and AsString since they have the same arguments. And I was able to make it work with the same name for collections and single objects (which arguably is not consistent with the deserialization from out last discussions) |
Those are good. |
I suggest that we don't use
I suggest we use class names without |
Is that really a big deal? I can only speak to C# and Typescript, but I cannot remember the last time I compared the using/import statements with the code in the body of a method. In fact, I only look at using/import when I first use a namespace. Having a repeated name in the namespace is way better than having to figure out the alias syntax of using. (Which, in fact, I had to look up right now to know what the name of the thing was.) I often use the fully-qualified name in the method to avoid this situation.
If I come across this scenario, I WANT to know the implementation details. I'm only here because the client does not do what I need. Just my thoughts... |
This is where I think we need to leak some of it. A library that is built with Kiota needs to be configured and customized with Kiota concepts anyways. In this case, I feel |
Thanks everyone for the great input here! I think we have everything we need, I'll move on the replicating to other languages now. |
For go we won't have the reflection methods as it's impossible due to the way things are compiled as far as I understand more information |
I'm also not going to add the string overloads as this is a trivial operation in Go that most developers are familiars with (string to byte[] and vice vera) |
Additionally, because Go (and TypeScript) has a notion of static functions (not attached to a class/struct), the naming I went with will be the following:
The alternative being to use the same name as the functions that require passing the content type, but being in a sub-package. This would work well for Go as you need to prefix symbols with their package import name (like |
I just submitted the PR for TypeScript. An additional note is that for both serialization and deserialization we need to as for the serializer and the deserializer function for the model. This is because interfaces get erased at runtime and there's no good way to get these methods by reflection. |
closing since it's been implemented in 4 canonical languages and related issues have been created for other languages. |
For anybody finding this issue while searching for a way to deserialize JSON to Kiota object model instance. Waldek's approach did NOT work for me: var bodyBytes = System.Text.Encoding.UTF8.GetBytes(body);
using var ms = new MemoryStream(bodyBytes);
var parseNode = ParseNodeFactoryRegistry.DefaultInstance.GetRootParseNode("application/json", ms);
var sitePage = parseNode.GetObjectValue(SitePage.CreateFromDiscriminatorValue); It throws:
Further googling lead me to this, which works: var jsonParseNode = new JsonParseNode(JsonDocument.Parse(body).RootElement);
var sitePage = jsonParseNode.GetObjectValue<SitePage>(SitePage.CreateFromDiscriminatorValue); Source was over here, at a similar issue: microsoftgraph/msgraph-sdk-dotnet#1708 (comment) Developers gonna develop. As long as I can google something and it works 😛 |
if you new up a client before calling |
@baywet Ah! Was not sure if that arrived in C#, yet. Looks good. But first-use experience for me also is this:
This is in unit tests, so there is no client yet. Running this before makes it work:
No one liner anymore, but nevertheless nice, that |
I have the exact pb. |
Assuming this custom deserializer is for JSON as well. Or you can call the dedicated registration method like in this example |
We should offer a helper method to help developers deserialize API response to an entity.
While in most cases we handle deserialization in the SDK, there are some exceptions to it:
While the first issue is exceptional, it's still blocking developers who need to wait for us to address the issue and release a new client.
Right now, deserializing API responses to entities require the following code:
This code isn't intuitive and requires quite some Kiota knowledge. We should aim to make it as simple as:
The text was updated successfully, but these errors were encountered: