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

A new way to modify JsonOptions #67097

Closed
1 task done
andrewjboyd opened this issue Mar 23, 2022 · 12 comments
Closed
1 task done

A new way to modify JsonOptions #67097

andrewjboyd opened this issue Mar 23, 2022 · 12 comments
Labels
area-System.Text.Json needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration

Comments

@andrewjboyd
Copy link

andrewjboyd commented Mar 23, 2022

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

I am trying to set the JSON options for specific requests on a legacy project. The global settings were set incorrectly, the options we have now are to:

  • define the new options and pass them manually to each new controller method. (more annoying than anything)
  • define the old settings and pass them manually to each of the existing controller methods, then set the global settings to what they should be set to. (rather scary)

Describe the solution you'd like

Define an abstract attribute like so

[AttributeUsage(AttributeTargets.Class)]
public abstract class JsonSerializerOptionsAttribute : Attribute
{
    public abstract JsonSerializerOptions JsonSerializerOptions { get; }
}

In the SystemTextJsonOutputFormatter, modify the code like so:

var serializerOptionsToUse = SerializerOptions;
var jsonSerializerOptionsAttributes = objectType.GetCustomAttributes(typeof(JsonSerializerOptionsAttribute), false);
if (jsonSerializerOptionsAttributes.Length == 1)
{
    serializerOptionsToUse = ((JsonSerializerOptionsAttribute)jsonSerializerOptionsAttributes[0]).JsonSerializerOptions;
}
else if (jsonSerializerOptionsAttributes.Length > 1)
{
    throw new InvalidOperationException($"Too many {nameof(JsonSerializerOptionsAttribute)} attributes on {objectType.Name}");
}

so you then pass in serializerOptionsToUse to the appropriate SerializeAsync call. Similarly in the SystemTextJsonInputFormatter.

By doing this, it would allow us to define the overriding settings in an class like so:

private sealed class MyCustomSettingsOverrideAttribute : JsonSerializerOptionsAttribute
{
    public override JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, };
}

So you can decorate your classes with your new Attribute, returning them however you want, without having to be locked into writing code like this everywhere:

return new JsonResult(result, MyCustomJsonSettings.Options);

Additional context

No response

@andrewjboyd andrewjboyd changed the title A new way to modify the A new way to modify JsonOptions Mar 23, 2022
@davidfowl
Copy link
Member

Where does this attribute go?

@andrewjboyd
Copy link
Author

It would go on the class being returned in the method, ie

[MyCustomSettingsOverride]
public class Example
{
...
}

Then in the controller:

public IActionResult<Example> Get()
{
    return Ok(new Example { ... });
}

or

var example = new Example { ... };
var exampleJson = JsonSerializer.Serialize(example);

@davidfowl
Copy link
Member

So you're trying to couple the serializer settings to the object being serialized? This sounds like a serializer feature, not an ASP.NET feature. Your last example makes that pretty clear.

@davidfowl davidfowl transferred this issue from dotnet/aspnetcore Mar 24, 2022
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Mar 24, 2022
@ghost
Copy link

ghost commented Mar 24, 2022

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Issue Details

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

I am trying to set the JSON options for specific requests on a legacy project. The global settings were set incorrectly, the options we have now are to:

  • define the new options and pass them manually to each new controller method. (more annoying than anything)
  • define the old settings and pass them manually to each of the existing controller methods, then set the global settings to what they should be set to. (rather scary)

Describe the solution you'd like

Define an abstract attribute like so

[AttributeUsage(AttributeTargets.Class)]
public abstract class JsonSerializerOptionsAttribute : Attribute
{
    public abstract JsonSerializerOptions JsonSerializerOptions { get; }
}

In the SystemTextJsonOutputFormatter, modify the code like so:

var serializerOptionsToUse = SerializerOptions;
var jsonSerializerOptionsAttributes = objectType.GetCustomAttributes(typeof(JsonSerializerOptionsAttribute), false);
if (jsonSerializerOptionsAttributes.Length == 1)
{
    serializerOptionsToUse = ((JsonSerializerOptionsAttribute)jsonSerializerOptionsAttributes[0]).JsonSerializerOptions;
}
else if (jsonSerializerOptionsAttributes.Length > 1)
{
    throw new InvalidOperationException($"Too many {nameof(JsonSerializerOptionsAttribute)} attributes on {objectType.Name}");
}

so you then pass in serializerOptionsToUse to the appropriate SerializeAsync call. Similarly in the SystemTextJsonInputFormatter.

By doing this, it would allow us to define the overriding settings in an class like so:

private sealed class MyCustomSettingsOverrideAttribute : JsonSerializerOptionsAttribute
{
    public override JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, };
}

So you can decorate your classes with your new Attribute, returning them however you want, without having to be locked into writing code like this everywhere:

return new JsonResult(result, MyCustomJsonSettings.Options);

Additional context

No response

Author: andrewjboyd
Assignees: -
Labels:

area-System.Text.Json, untriaged

Milestone: -

@eiriktsarpalis
Copy link
Member

What's the use case? Do you need to specify a naming policy that is scoped to the particular type? Something else?

At the moment I don't believe it is technically possible to scope a JsonSerializerOptions instance to a particular type (the internal converter infrastructure generally assumes a singular instance reused throughout the object graph). Even if we tried to make it happen the custom options instance would flow to nested values which would likely result in surprising behavior.

@eiriktsarpalis eiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Mar 24, 2022
@ghost
Copy link

ghost commented Mar 24, 2022

This issue has been marked needs-author-action since it may be missing important information. Please refer to our contribution guidelines for tips on how to report issues effectively.

@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Mar 24, 2022
@andrewjboyd
Copy link
Author

It would actually be for many types. There are two scenarios I've come across, one where I worked on a project that was set up by a developer with pascal case being returned through the API but was being consumed by a javascript app we wanted to refactor it to be camel case and all new development would use camel case, so we had to use return new JsonResult(result, MyCustomJsonSettings.Options); everywhere to get this result. The other scenario, is we have IoT devices talking to our API, but they expect pascal case, so that subset of API endpoints need one set of options, while the others need pascal case.

I've got the code working on my work laptop, I'll test if nested objects behave weirdly, you're right:

{
  test1: 1,
  test2: 2,
  Test3: {
    Test3_1: 1
  }
}

would be pretty different, but it should only look at the root object for it's JsonOptions

@ghost ghost added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed needs-author-action An issue or pull request that requires more info or actions from the author. labels Mar 24, 2022
@andrewjboyd
Copy link
Author

I double-checked, the code sample at the top of this only looks at the root for the JsonOptions and ignores any associated with child objects, if they exist

@krwq
Copy link
Member

krwq commented Mar 25, 2022

@andrewjboyd take a look at #63686 and see if that would be satisfactory. I know this speaks about attribute to override options but ultimately goal seems to be to have more customization and perhaps contract resolver would be a better way to achieve what you want to do.

@andrewjboyd
Copy link
Author

@krwq, if I understand correctly, you would be able to have it output something like this right?

{
  test1: 1,
  test2: 2,
  Test3: {
    Test3_1: 1
  }
}

Is there any sample code for this .NET 7 feature? I'm trying to understand how I could achieve what I'm trying to achieve

@eiriktsarpalis
Copy link
Member

@andrewjboyd the feature described in #63686 is currently under development so we can't provide concrete workarounds, but yes, it would be possible to set a custom naming policy for individual types or properties. For the reasons I outlined above, it would not be possible to specify a custom options instance on the type level.

I'm going to close this in favor of #63686.

@andrewjboyd
Copy link
Author

Thank you for your time @eiriktsarpalis, @krwq & @davidfowl. I can't wait to see this new feature up and running!

@ghost ghost locked as resolved and limited conversation to collaborators May 4, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Projects
None yet
Development

No branches or pull requests

7 participants