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

[For Discussion] Move JsonData to Core #31769

Closed
wants to merge 31 commits into from

Conversation

annelo-msft
Copy link
Member

@annelo-msft annelo-msft commented Oct 12, 2022

Context

Requirements

The main work needed, given that we have the prototype, is to:

  • Remove mutating APIs.
  • Decide how arrays work (see above)
    There is a test showing this capability in the old PR: CanAccessArrayValues
  • Decide how casts work, e.g. var date = (DateTime)json; or cast to custom model: var model = (FooModel)json.Foo
    There is a proposal regarding an approach to enabling this in this commit. It suffers from the issue that it uses JsonSerializer.Deserialize<T> to do the conversion between JsonData and T, and we know that not all of our Azure SDK models are serializable via STJ.
  • Decide how roundtripping would work. It will not be very easy/usable, but it should be possible.

Suggested approach (tracked separately by #31943):

// ***
// Round-trip: JsonData and Anonymous types
// ***
Response current = await client.GetKeyValueAsync("FontColor");
RequestContent updated = RequestContent.Merge(current, patch:
    new
    {
        value = "updated value";
    }
);
dynamic keyValue = new JsonData(current.Content);
Response putResponse = await client.PutKeyValueAsync((string)keyValue.key, updated, ContentType.ApplicationJson);
  • Implement a good debugger visualizer

image
If this isn't "good" yet, let's clarify the ask.

  • Update our protocol method docs
    We will file issues to track this.

Code sample

Per this comment:

The output of:

dynamic json = JsonData.Parse(new BinaryData(new
{
    Foo = new
    {
        Bar = "hi"
    }
}));
Console.WriteLine(json.Foo);
Console.WriteLine(json.Foo.Bar);
Console.WriteLine(json);

dynamic json = JsonData.Parse("5");
Console.WriteLine(((int)json) + 5);

dynamic json = JsonData.Parse("[1, 2, 3, 4, 5, 6]");
Console.WriteLine((int)json[5]);

is

{"Bar":"hi"}
hi
{"Foo":{"Bar":"hi"}}
10
6

@ghost ghost added the Azure.Core label Oct 12, 2022
@azure-sdk
Copy link
Collaborator

azure-sdk commented Oct 12, 2022

API change check

APIView has identified API level changes in this PR and created following API reviews.

Azure.Core
Azure.Core.Experimental

@KrzysztofCwalina
Copy link
Member

If this isn't "good" yet, let's clarify the ask.

I worry that this view does not scale to large JSON payloads.

@@ -91,7 +89,7 @@ public void AnalyzeConversation()
#endregion

Assert.That(response.Status, Is.EqualTo(200));
Assert.That(conversationPrediction.GetProperty("topIntent").GetString(), Is.EqualTo("Send"));
Assert.That((string)conversationPrediction.TopIntent, Is.EqualTo("Send"));
Copy link
Member Author

Choose a reason for hiding this comment

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

A string cast is required here for this to succeed right now.

#if !SNIPPET
aspects.Add(summary.GetProperty("aspect").GetString());
aspects.Add((string)summary.Aspect);
Copy link
Member Author

Choose a reason for hiding this comment

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

A string cast is required here to succeed.

/// </summary>
/// <param name="utf8Json">A UTF8 encoded string representing a JSON value.</param>
/// <returns>A <see cref="JsonData"/> representation of the value.</returns>
internal static JsonData Parse(BinaryData utf8Json) => new JsonData(JsonDocument.Parse(utf8Json));
Copy link
Member Author

Choose a reason for hiding this comment

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

Handle IDisposable properly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Per @AlexanderSher:

I've created a small benchmark. It looks like disposing JsonDocument is quite important:
https://gist.github.com/AlexanderSher/251a035cfbbe539337e4a1c6504ff52b

Copy link
Member Author

Choose a reason for hiding this comment

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

@AlexanderSher, does this address your concern on this? 795e2ff


Console.WriteLine($"Top intent: {conversationPrediction.GetProperty("topIntent").GetString()}");
Console.WriteLine($"Top intent: {conversationPrediction.TopIntent}");
Copy link
Member

Choose a reason for hiding this comment

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

Discussed with @KrzysztofCwalina offline, but I don't think we should automatically convert camelCase JSON properties to PascalCase .NET properties on responses. Requests don't get the same treatment, so this may be confusing or even misleading. Say you get a response back, want to update it, and then send it back (which you also can't since JsonData is effectively immutable):

var json = response.Content.ToDynamic();
var data = new
{
  Id = json.Id,
  Foo = "new foo",
};
client.UpdateFoo(Request.Create(data)); // throws RequestFailedException: 400 BadParameter or something because the properties are supposed to be camelCase.

I appreciate we want to be idiomatic, but as soon as customers are having to deal with raw JSON - via System.Text.Json like my samples were before vs. what you're prototyping here with JsonData (which is a remarkable improvement) - I don't think we should "auto-correct" anything. Python nor JS would since customers are working directly with the JSON data. I don't think C# / .NET should either.

Model classes give us this ability, and once we generate them (for Cadl or otherwise) we can and should do that translation because we can do so consistently.

/cc @tg-msft

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess I don't understand the thinking there. We do this in our public models, and the idea is that JsonData is a stand-in until we add those types. Ideally, the customer would have to change very little to move to any types we added to a library. It's not that they're dealing with raw Json (which they can do easily if they want) -- it's a layer over the Json (or Xml, or whatever) that makes it conform to a higher .NET standard for look-and-feel. Is consistency with anonymous types your primary concern here, because I agree that is not good. Are there any other considerations I'm not thinking of?

Copy link
Member Author

@annelo-msft annelo-msft Nov 8, 2022

Choose a reason for hiding this comment

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

Ah, I see that this is a question about round-tripping the type, my apologies for having missed that the first time. We are planning to provide a separate API to handle-round tripping, that may be able to address this. I am still curious to know if there are other considerations here beyond the inconsistency across input/output that we should also be aware of.

Copy link
Member

Choose a reason for hiding this comment

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

Assume with my example above that the JSON schema is indeed camelCase, as most services define (and should): the request would accept id and foo, not Id and Foo. However, by translating from camelCase to PascalCase we could give callers the impression that the properties are indeed Id and Foo.

This isn't just about roundtripping, though. It's about perception. If all the response properties are PascalCase, why wouldn't many think that request properties are too?

I appreciate this isn't "raw" JSON. That's not what I intended. But it's a very thin wrapper over raw JSON that is meant to provide a JSON-like interaction for the customer, yes? By morphing into something it's not just to seem idiomatic, we create this confusion of casing for customers. Why not just leave it with the exact shape - casing and all - as the service returns (response) and expects (request)?

Copy link
Member Author

@annelo-msft annelo-msft Nov 8, 2022

Choose a reason for hiding this comment

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

That's totally fair - all good points. It feels like the question hinges on how thin the wrapper is and how well we document and illustrate via samples what the type is actually doing. If, for example, it also wraps other formats like XML, it might be more clear that you can't make too many assumptions about what's underneath.

Copy link
Member

Choose a reason for hiding this comment

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

If this supports XML, I don't see my point changing: if an XML document uses camelCase or PascalCase (and in my experience it varies even more than with JSON, which is more often camelCase), we should leave casing as is to avoid confusion. If we start showing that customers can always use PascalCase for responses, why wouldn't they think they can use it for input as well? Or if they use the obj["foo"]["bar"] pattern it will also vary from obj.Foo.Bar. Changing cases like that just seems so jarring.

What benefit to the customer does translating the casing have? I can certainly see a lot of cons. The only pro is "it feels like C#", but they aren't working with generated types (C# or otherwise): they are working with thin wrappers over raw JSON, XML, etc.

Copy link
Member

Choose a reason for hiding this comment

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

Should ToDynamic take an optional ObjectSerailizer so folks can customize this? I feel like that would easily allow this to work for people who wanted it, but then we could get away with less magic by default if we didn't change the case.

@annelo-msft annelo-msft changed the title Move JsonData to Core [For Discussion] Move JsonData to Core Nov 17, 2022
@ghost ghost added the no-recent-activity There has been no recent activity on this issue. label Jan 20, 2023
@ghost
Copy link

ghost commented Jan 20, 2023

Hi @annelo-msft. Thank you for your interest in helping to improve the Azure SDK experience and for your contribution. We've noticed that there hasn't been recent engagement on this pull request. If this is still an active work stream, please let us know by pushing some changes or leaving a comment. Otherwise, we'll close this out in 7 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Azure.Core no-recent-activity There has been no recent activity on this issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants