-
Notifications
You must be signed in to change notification settings - Fork 39
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
Models should implement std lib's JSON Un/marshaler interfaces #371
Comments
I imagine your code is autogenerated, so you may not care about repetition across files. If you do care, I realized you can save yourself some trouble by leveraging generics func UnmarshalJSON[T serialization.Parsable](b []byte, model *T, parser serialization.ParsableFactory) error {
jpn, err := kjson.NewJsonParseNode(b)
if err != nil {
return err
}
v, err := jpn.GetObjectValue(parser)
if err != nil {
return err
}
*model = v.(T)
return nil
}
//...
func (m *ChangeNotificationCollectionResponse) UnmarshalJSON(b []byte) error {
return UnmarshalJSON(
b, &m, models.CreateChangeNotificationCollectionResponseFromDiscriminatorValue,
)
} |
Hi @eric-millin, You are correct, this is generated using kiota. Marshalling JSONThere are a number of reasons why we didn't enable support for this scenario:
Ultimately this SDK should be an infrastructure layer (in DDD terms) and the service should be encapsulated, allowing for easy mocking and standard communication with the other layers through application layer models. Generics usageWhile working to improve this preview SDK, we actually did try Go generics. Our findings were that with such a large codebase the memory pressure on the compiler increased by a lot, making builds fail. After lots of considerations we decided to roll the use of generics back. You can see our progression in #129 With that additional context, is there something we're missing in terms of scenarios that might not be possible today? |
Thanks for the speedy reply and detailed answer. I asked my team as a sanity check and everyone was very surprised that the structs don't support using the stdlib's I certainly understand why you implemented your own deserialization approach. However, I'm missing what prevents implementing the As to why someone would want to deserialize in the first place, the simplest example I can give is in our endpoint for change notifications. We get a notification blob and we want to unmarshal it to an SDK model. I expect to be able to do that with |
Here's my example implementation that would seem to satisfy your requirements func (m *ChangeNotificationCollectionResponse) UnmarshalJSON(b []byte) error {
jpn, err := kjson.NewJsonParseNode(b)
if err != nil {
return err
}
v, err := jpn.GetObjectValue(models.CreateChangeNotificationCollectionResponseFromDiscriminatorValue)
if err != nil {
return err
}
*m = *v.(*ChangeNotificationCollectionResponse)
return nil
} |
Thanks for the additional context here. This approach (customizing the default platform serialization behavior) was the approach we were using for the current dotnet and java SDKs (non kiota based). It led at best to convoluted code, at worst to inaccuracies with the serialized/deserialized results. And it ties the model to specific serialization format. Have you tried using the JsonParseNode to handle the deserialization of such types for you automatically? You could do so by implementing the Parsable interface (like other models) until we ship types for change notifications themselves. |
I think so: this is the pattern we're following now for change notifications jpn, err := kjson.NewJsonParseNode(b)
if err != nil {
return err
}
v, err := jpn.GetObjectValue(models.CreateChangeNotificationCollectionResponseFromDiscriminatorValue)
if err != nil {
return err
} So, there is a way to do what I want, but it doesn't enable me to follow Go conventions. Clearly I don't understand how the files are generated, which I imagine isn't easy to put together. However, as an outsider it doesn't look difficult to write a script that would iterate the struct files in the |
Besides the potential difficulty it'd transgress a tenant of Kiota client generation that models only rely on kiota abstractions and they are not tied to any specific format or serialization "library". Kiota rolls out with default implementations people are more than welcome to use, but can choose to re-implement should they not be happy with them. So we're not likely to change that aspect now unless we get overwhelming feedback we should. Besides adding this would increase the SDK size significantly which has proven to be a challenge. I do believe that part of the issue here is that no example is provided to handle change notifications in Go, like we have in java and other languages. But it's on our roadmap once we fix the missing types and once this SDK GAs. I hope that clarifies our plans and the rationale behind them. |
We experienced some of the side effects of that challenge. Many thanks for the recent changes that made our builds faster and dev experience better. Re examples, are there any Go examples of subscription token validation? I'm referring to something analogous to this Java code: |
Thanks for the kind words. Not at the moment (at least not published by Microsoft) but it's something on the documentation team that'll happen once we include the missing types and once the SDK GAs. |
Thanks for all the help. Closing the issue |
@baywet Just wanted to share one bit of related feedback that I got during code review. Coworker specifically pointed out the need for custom deserialization code and referred to the SDK as "clunky and non-idiomatic". There are a lot of really great things about the SDK. The abstractions make many things easy, particularly leveraging polymorphism. However, at some points I think you'll need to make it more Go-ish. Thanks again for your help and thoughtful consideration of the issues I raised. |
thanks for the feedback. Would it help if the serialization package had a generic marshall/unmarshall function? user := NewUser()
//do stuff to user
result := marsall(user)
//byte array
// marsall function comes from github.com/microsoft/kiota-serialization-json-go This way it'd save people from having to understand the different layers of the client? |
@baywet I didn't see this last response, apologies. I think a helper like that would be a good improvement. I really don't want to be "that guy" who's always harping on the same thing and being annoying as hell, so I'm fine moving on. However, if you have the patience to indulge me, maybe you can explain how wrapping your proposed EDIT: I realized that part of what I don't get about the design is that from the user perspective, the structs are bound to a serialization format by their very existence. The Graph API is JSON, so it seems like it's a no-brainer to make JSON support simple, easy, and idiomatic. What I envision is roughly func (user User) MarshalJSON() ([]byte, error) {
// your helper is used here
result := marsall(user)
// etc
}
// now I can do things the Go way
user := NewUser()
//do stuff to user
b, err := json.Marshal(user) |
FWIW, this is how I do things now, but it sounds like you're saying there's a fragility here that you want to avoid. That makes me wonder if I want to avoid it too... func UnmarshalGraphModel[T serialization.Parsable](b []byte, model *T, parser serialization.ParsableFactory) error {
jpn, err := kjson.NewJsonParseNode(b)
if err != nil {
return err
}
v, err := jpn.GetObjectValue(parser)
if err != nil {
return err
}
*model = v.(T)
return nil
} |
to clarify my points: We are not going to add marshalling functions to each model. This would tie models to serialization formats which goes against our design principles. Microsoft Graph serializes things to text/plain, application/json, application/x-www-form-urlencoded and a few other content types depending on which endpoint and OData convention we're talking about. So our design has to account for that. The fact that our design didn't account for it in previous generation SDKs (dotnet, java...) was causing serious limitations. In additions, kiota, the generator this SDK is based on, might be used for APIs serializing to XML, or other formats. A single set of static functions to handle marshalling generically for all models is a good approach, I wasn't suggesting it was brittle or anything. I think the confusion came from the previous point. What we could do is have those functions in the JSON package itself, or better yet, would you be willing to submit a pull request to add them there? |
Gotcha, thanks.
I'd be happy to. Is the generic function I shared above + tests sufficient or do you have something else in mind? |
no that looks great as it is, only the doc comments are missing (and the tests). Thanks for the contribution! |
This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment. |
I was writing tests and was very surprised to find that the models can't be de/serialized using standard Go conventions built around the std lib.
I did a little testing and found that the std lib's interfaces are easy to implement as wrappers around the existing deserialization code. Here's what I go working for a mock
ChangeNotificationCollectionResponse
.The code above is a poc implemented in my own project. I believe you should be able to do it even more simply
Add the
UnmarshalJSON
wrapper then allowed me to usejson.Unmarshal
.The text was updated successfully, but these errors were encountered: