-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Api proposal: Change JsonSerializerOptions default settings #31094
Comments
Can you expand on why that is a concern for you and what scenario is blocked by the current design (preferably with some sample code)? Given the |
JSON properties are conventionally The solutions today, as far as I'm aware, all involve passing things around:
Now, of course one could argue that the correct way is indeed to have case-sensitivity, since you can always have JSON like A better option, perhaps, would be to inform the serializer that by default all properties are to be assumed All of our projects currently expect this behavior. Changing this wouldn't be a deal breaker for adopting If there was an instance, non-static version of |
Why don't you declare the default setting in a static class and then just do
That's what I did to avoid passing settings around but making sure it's getting serialized the same everywhere in project. If your wish is to set the settings ONCE at start you already know how the settings should be so static is fine. Otherwise if you load the settings, so they are unknown before application start, it cannot be static itself but you could do the same but with a static reference to an instance:
|
Sure, but that still kind of have the same issues as passing it around everywhere, except they're not a method parameter. You still have to remember to do it, and thus it's still error prone, and it still feels quite redundant to be passing the "default" options to every single method call. If it's supposed to be the default, why do I need to specify it every single time? It's like having C# optional parameters, but still having to specify them in every call. |
Because of what ahsonkhan said. Its a static class with static methods for performance reasons. You want add performance decreases by making the method not static only to avoid remembering to add "DefaultJsonSettings.Settings" every time you use it. This seems like a really bad trade, especially for the great majority where the current way it works isn't a big deal. If this is such a big deal for you, why don't you just write DefaultJsonSettings.Settings once and put that inside a static method and call this instead, problem solved.
And please do not say you don't want this because you have to remember using Bla.SerializeObject instead of JsonConvert.SerializeObject |
How so "please do not say"? Am I not allowed to express my opinion? By saying so, you show that you do understand the issues I'm raising and you do understand that they're present in the ""solution"" you suggest. That's dishonest. Remember that different people have different needs, works in different projects and under different scenarios. Your opinion is not golden. It's just as valid as mine. If my suggestion doesn't prove to be much useful, don't worry, it simply won't get traction and won't get implemented. Don't think there's a need for you to harass anyone.
I want no such thing. Never said it. I'm merely asking for a way to have default settings. The implementation is up for discussion.
It's not "only" to avoid remembering. When you argue like that, you can make any argument look pointless. As I said in previous replies, this is simply a point of great coding practices. Anyone who owns a codebase and see a thing such as having a method being called all around the solution every time with the same argument would do something about it. And again, this is because it is simply a point of great coding practices. It's easy to see the point.
Only if it has to be that way. I too agree that if a non-insignificant performance impact is required in order to do this, it wouldn't pay itself, and I wouldn't support it. @ahsonkhan Can't we just create a new overload that doesn't have the |
Just as a quick note, I think it's also worth remembering that another benefit of having a static property is that you're able to change the serializer behavior for all assemblies in the running program. I don't depend on this behavior currently though, so I can't say how much of a benefit that would be. I guess it could be useful in scenarios where you have code over which you have no control that uses |
I really think this is a huge trade-off. It can be a huge pit-of-failure in libraries which interact with web APIs which expect a specific casing. Of course, they should be written in a way which is not vulnerable to this, but by making it the "default", it makes it a lot easier for bad code to be written. Personally, I don't really think specifying JsonPropertyName for every property name is a negative - in fact, I feel that explicitly specifying property names is advantageous, even if it can seem like code duplication. |
I understand your point. However, I'm not sure if you misunderstood me or if I'm misunderstanding you 😄 but I'm not advocating for that behavior to be the default; instead, only to give users the option to have a default, whatever that might be. Having said that, please note then that 1) you're focusing on case-sensitivity, when that was merely the reasoning behind my need for this. This issue is not about case sensitivity, it is about offering a way to change the defaults only once. No one is asking here for case-insensitive to be the default behavior anywhere and of anything; and 2) your point boils down to "giving developers a choice means they will write bad code and thus let's not give them a choice". And although I can see an argument like that being applied elsewhere, I don't really think it applies here. I'm still going to reply to each one of your points, but I think this (whether or not case-sensitivity should be the default) is mostly something not worth arguing over, as it is not the point of this issue. Please note that this has existed in JSON.NET for many many years and I don't think a lot of "bad" code has been written because of it. In fact, in JSON.NET, that behavior (case-insensitive) is the default behavior (but again, I am not saying it should be, I'm merely bringing that fact to light).
First of all, notice that I'm more interested in the deserialization behavior than the serialization one. I don't really care how the payload is going to be serialized. As said above, 1) I am not advocating for this to be the default behavior; and 2) this has existed for years and I don't really see this ever being a problem. And, in fact, as I said before, I actually think it's the other way around: .NET developers are more familiar with cC -> CC because of the clash between the .NET naming guidelines and JS naming conventions. And that's not really a problem at all, since, as mentioned before, people simply don't go around there sending Also, please note that this is the default behavior for MVC (and still continues to be, even after System.Text.Json: dotnet/aspnetcore#10723) and, again, people are not writing "bad" code because of it. And, most importantly, MVC was engineered in a way where you can, in fact, change the default only once.
So I don't think there is really any trade-off here (in the context of what you said -- note that I do understand that there may be some trade-offs in terms of performance and/or API design!). By having the option to specify the defaults in System.Text.Json, not only it would give us more flexibility, since - and this is important to note - there are other behaviors besides case-sensitivity that can be controlled through the options, but it would also pave the way for a more seamless migration between the two libraries.
Right. I'm a big advocate of the idea of being explicit. However:
|
The feature ask is valid IMO and was discussed early on when designing Currently the "default" option is on a private static variable. This means it can only contain default settings. Also, FWIW, we also discussed add assembly-level attributes that can configure things like the naming policy. This would support some of the scenarios without having to specify where the default option lives. |
((JsonSerializerOptions)typeof(JsonSerializerOptions)
.GetField("s_defaultOptions", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic).GetValue(null))
.PropertyNameCaseInsensitive = true; 😈😈😈 |
The solution @andre-ss6 came up with above using the internal default settings demonstrates that this is a real issue. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
It should be a given that .net let us set default settings for the serializer, having to repeat the options everywhere or use wrapper classes is too error prone and will cost more than slight performance regressions. Newtonsoft it is until this is fixed. |
But setting PascalCase ONCE in the STATIC json serializer options is what I and everyone else moving from MVC5 to Core 2.2 then onto 3.0 will want to do. Because we've already written the code like that (PascalCase in C#, camelCase in Javascript), have an awfully large number of classes it affects and have no intention of going round and applying another attribute to everything and hoping we got them all. It's EXACTLY what the op is asking for, so very on topic. I also just want to point out to those coming here that there is an option, which is NewtonSoft JSON can be added back in as middleware and be set to work exactly as it used to, so we can move to .Net Core 3.0 regardless of this substantial oversight by kicking out this implementation of a Json Serializer. Don't get me wrong, I'd like to use it, but without what the op has asked for - very unlikely. |
Ran into this issue when trying to create an asp.net web API with matching .net core console client. The default behavior of the serializer is to use Passing in the same options value across the entire application seems like a huge violation of DRY and an easy way for bugs to creep in. |
The default behavior of Within aspnet however (which includes Mvc, SignalR, Web Api), the That is likely why you are seeing the observing the discrepancy. Aspnet configures the defaults differently. If your .NET object graph happens to contain You would have to override the options in either in your web api or in your console client to make sure they are consistent (you probably want your console clients json serializer options to be configured to match the web api serializer options). Would having a globally configurable serializer help in your scenario? If the two contexts are in separate assemblies, you would have to configure both anyway, no? |
I just wanted to add a comment here that adding case-insensitivity appears to increase memory consumption and degrade performance on very large payloads. Since there does not appear to be a way to turn off case sensitivity with Newtonsoft deserialization, I am unable to compare to that with it disabled. My memory as reported by BenchmarkDotNet was 2.5MB deserializing this massive stream and case insensitive enabled it went to 5.5MB. Performance was 20ms without and 26ms with. For comparison, Newtonsoft is currently taking 32ms to deserialize the stream and consumes 3.5mb. Default buffer size is set to 100KB. |
@jtreher can you share the code for that benchmark? |
@andre-ss6 My payload has a lot of data in it that would need scrubbed. The benchmark is pretty simple. You can either set the case insensitive in the benchmark method or just flip the flag and run the benchmarks again and you'll see the decline in performance insensitive vs. sensitive. My "response.json" is 1.5 MB for reference -- 100 very large and several nested levels deep objects.
|
Sincere question — how much design complexity would be introduced from this one particular proposed divergence from a static class containing purely static methods? I acknowledge there would be trade offs. I'd like to understand their magnitude. On a separate note, a few different ways to approach setting the defaults in one place have been suggested on the related Stack Overflow question. |
As I understand it, the tension is more on the design side than performance considerations. I think they're not sure yet if they want to expose a static property. |
Thanks @andre-ss6. I had misread @ahsonkhan's comment at the head of the thread. I've updated my inquiry to reflect the more pertinent trade off. |
Yea, it would be nice if they share that info with us. Simply saying there is complexity involved but then leaving us in the dark is not helpful. I would hope we could help in the discussion. Simply requesting a feature and waiting in the dark feels like UserVoice. |
Alright, taking a stab at it. Static defaults property (publicly expose today's field):
Static generator property (JSON.NET):
Assembly attribute:
New C# 8.1 feature where you tell the compiler that you want a optional parameter to always have the same value:
|
Apologies for that. I had been working through other JSON issues, since the backlog of feature requests is relatively large (and as you probably know, context switching is difficult). I will try to be more responsive here. Can you update the OP with the exact API shape/change you'd like to propose that will address your concern and then we can discuss it. Once ready-for-review, we can take it to API review in the new year. Having the caller override the default I have a few questions that are worth looking into as part of the spec/requirements:
There is clearly a vocal interest here. Would you (@andre-ss6 ) like to take a shot at an implementation as part of the design, especially since you have already put a lot of thought into this? While we wait for the API to get reviewed, I would be happy to take a look at a draft/WIP PR which helps showcases the feature in action along with tests to help design this. I would only do this after we have consensus on the this thread around the api proposal and it is marked as api-ready-for-review. Moving this from future to 5.0. |
@jtreher, were you running on 3.0 or 3.1? Can you try running them again on 5.0 and share your results, especially the difference in memory allocation? Also, is the sample model/payload representative of what you actually use and see in practice? I understand you can't share the payload, but can you share your
I am asking because I tried deserializing a 150 MB payload from The perf difference is expected given how the caching of property names works currently. We have certain optimizations that work when we know exact comparisons are being done, which don't work for case insensitive comparisons (so we have more calls to |
@krwq and @steveharter, being able to put an annotation on a class could be useful to some people, but it would be really nice to be able to set the assembly level. Are we at the point where someone needs to write up an API proposal to move things forward? |
Next steps here are to design specific APIs (and if proof of concept is needed create a prototype). Any volunteers? We're currently still in a bug fixing/planning stage so we're not sure if we'll have time to pick this up in 7.0 without some help. |
No one has agree anything, nothing has to be one way, don't commit to the path that has been there unless is necessary, by that it usually means solid logical constrains, if there isn't one then possibility shouldn't be limited. Are you so smart to guaranteed it's a bad thing 5 years later, don't limit yourself until others proves you wrong since it's none lethal just like we as a society is making progress having multiple gender identities not instead because of we are biological different. |
😁I would be careful with this implementation though. In the case that a serialization already occurred, you're likely to get an exception. |
@0xced Hey Mr-Know-It-All. Pardon me. :) Your use case is different. And read the author's original intention again, please. Implementation is open for discussion. |
FWIW that particular snippet is going to break in .NET 7, since the default instance has been refactored to an auto property. |
@nguyenlamlll |
Want to add that this is especially a concern for me when writing Being able to set a more sane default for But perhaps there's greater concerns that block making this issue from being resolved? EDIT: Right now I contemplate doing something like this to support my xUnit API Integration tests: private static readonly JsonSerializerOptions jsonSerializerOptions
= new JsonSerializerOptions(JsonSerializerDefaults.Web);
static HttpClientExtensions()
{
jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
} to use it along these lines: public static async Task<T> GetAsync<T>(this HttpClient client, string url)
{
return await client
.GetAsync(url)
.ReadAsDeserializedData<T>();
}
private static async Task<T> ReadAsDeserializedData<T>(this Task<HttpResponseMessage> responseTask)
{
var response = await responseTask;
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
if (data == null) throw new Exception("Unexpectedly encountered null response body.");
return data;
} 😢 |
@InsomniumBR wouldn't following https://docs.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-6.0&tabs=dotnet work for you? |
@krwq in fact none worked. I thought my problem was in the System.Text.Json serialization. And that line of code worked in azure function constructor or inside the function method to make the JsonSerializer.Serialize works as I expected (using the converter). But didn't solve my problem anyway. I need to dig more here to understand with this SignalROutput is not using these settings, or if I am missing something: |
@steveharter @krwq I'm willing to help move this issue forward with an API design and a prototype as I have a need for this functionality in my own projects. How were you envisioning the solution with assembly level attributes working? public static JsonSerializerOptions Default { get; } = CreateDefaultImmutableInstance(); to public static JsonSerializerOptions Default
{
set
{
string callingAssembly = Assembly.GetCallingAssembly().GetName().FullName;
if (_defaultOptions.ContainsKey(callingAssembly))
{
ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(context: null);
}
value.InitializeCachingContext();
_defaultOptions.Add(callingAssembly, value);
}
}
internal static JsonSerializerOptions GetDefaultOptions(string callingAssembly)
{
return _defaultOptions.TryGetValue(callingAssembly, out JsonSerializerOptions? value) ? value : _defaultOption;
}
private static Dictionary<string, JsonSerializerOptions> _defaultOptions = new Dictionary<string, JsonSerializerOptions>();
private static JsonSerializerOptions _defaultOption = CreateDefaultImmutableInstance(); Then the options could vary depending on which assembly is calling into public static TValue? Deserialize<TValue>([StringSyntax(StringSyntaxAttribute.Json)] string json, JsonSerializerOptions? options = null)
{
if (json is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(json));
}
// Insert this in every public api method that takes a `JsonSerializerOptions?` input parameter
options ??= JsonSerializerOptions.GetDefaultOptions(Assembly.GetCallingAssembly().GetName().FullName);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue));
return ReadFromSpan<TValue>(json.AsSpan(), jsonTypeInfo);
} Some quick benchmarking seemed to indicate that the performance hit from the third and any subsequent call to |
I think that might be worse than having a global setter. It absolutely has the potential of catching users by surprise. |
@eiriktsarpalis I totally get that and agree to some extent. I guess I am a little confused on what the sentiment is. Are we at a point where you are willing to accept a solution with a public static default options (mutable or immutable), or should the solution exhibit other characteristics/behavior, and if so which? |
.NET 7 will expose an immutable default instance (via #61093). For reasons already stated in this thread, I don't believe we would ever consider making it settable. One possible alternative might be supporting "JsonSerializer" instances: an object encapsulating |
Then I will reiterate @IanKemp's suggestion (#31094 (comment)): public interface IJsonSerializer
{
Task<T> DeserializeAsync<T>(Stream utf8Json, JsonSerializerOptions options, CancellationToken cancellationToken);
// other methods elided for brevity
}
public class DefaultJsonSerializer : IJsonSerializer
{
private JsonSerializerOptions _defaultOptions;
public DefaultJsonSerializer(IOptions<JsonSerializerOptions> defaultOptions)
{
_defaultOptions = defaultOptions.Value;
}
public async Task<T> DeserializeAsync<T>(Stream utf8Json, JsonSerializerOptions options = default, CancellationToken cancellationToken = default)
=> await JsonSerializer.DeserializeAsync<T>(utf8Json, options ?? _defaultOptions, cancellationToken);
} Is this closer to something you will consider @eiriktsarpalis? |
Something like that, although probably without the interface and IOptions dependency. I also think that the instance methods should not accept an options parameter to emphasize encapsulation. At risk of stating the obvious, such a class can easily be defined by users and would introduce duplication of serialization APIs. It's a fairly drastic intervention providing comparatively low benefit and as such we should not do it unless we are absolutely convinced it's the way forward. |
I think as a stop gap, registering in DI a Most of my use cases have been either integration testing an AspNetCore app, or within the app itself where I want to do some logging of some sort, or persisting Json into external storage systems. |
Here's another reason why we might want to consider introducing a The virality of @krwq @eerhardt @jeffhandley thoughts? |
I chatted a bit with @vitek-karas about this yesterday. Being able to put all the "JSON source gen information" (i.e. JsonTypeInfos / JsonSerializerContext, or an instance of JsonSerializer) into DI makes a lot of sense to me. This could be a nice way for ASP.NET to use the JSON source generated code when they serialize objects. One scenario that might need thinking about is if you want to serialize the same Type in multiple ways - lets say PascalCase in one case, and camelCase in another (or any other option that the source gen respects - like WriteIndented). Either way - I think having a "DependencyInjection-friendly" way of serializing objects makes sense and would be valuable. This could be to make One more issue is that you'd still want the default experience to work in ASP.NET, when JSON source generation isn't used at all. That means a "fallback" or "default" experience (i.e. Reflection) needs to still work for "normal" apps. However, this "fallback" or "default" code will raise trimming warnings. To get rid of those, we could introduce a feature switch that removes the Reflection experience and ONLY works with trim-safe serialization code. Then your app wouldn't use Reflection to do serialization at all. cc @davidfowl - FYI since we've discussed this "how can ASP.NET use JSON source generation code" topic a lot. |
I think another important capability which ASP.NET would probably make use of is the ability to "merge" several such instances. For example let's say my app uses a library FabulousObjects which contains source generated serialization code for its objects because it calls into ASP.NET on my app's behalf. But my app also contains other source generated code for app's objects, which again are serialized by ASP.NET. For this to work both the app and the FabulousObjects would need to register the source generated code with the DI - but the fact that there are 2 sources should be transparent to ASP.NET itself, thus the need to "merge" the two instances into one which can serialize objects from both the app the library. Such design would probably work better if we DI the contexts which can be merged, but maybe there's a way to do this with serializer instances themselves. |
Having an "instance" serializer makes a lot of sense to me. The interesting pivot point to me is the concept of having a "serializer" vs a "serialization context". What I mean by that is that we could consider three separate levels of serialization state. At the highest level we have a static The next level is a kind of serialization context, which bakes in some set of options. This version would allow you to preserve options between invocations, but wouldn't preserve any information about the serialization process itself. The last level is the actual serialization process. This is an inherently stateful operation where the output and input are held in intermediate state as we walk the type graph (or text in the case of deserialization). This level would fix the input and the output and would be essentially one-shot, not reusable across multiple serializations. Right now in serde-dn I have support for the first and last abstraction levels, but not the middle one, i.e. this is the implementation of JsonSerializer.Serialize, which is missing a set of options: public static string Serialize<T>(T s) where T : ISerialize
{
using var bufferWriter = new PooledByteBufferWriter(16 * 1024);
using var writer = new Utf8JsonWriter(bufferWriter);
var serializer = new JsonSerializer(writer);
s.Serialize(serializer);
writer.Flush();
return Encoding.UTF8.GetString(bufferWriter.WrittenMemory.Span);
} One way to fix this would be to introduce a The tricky bit in my mind here is the source generation stuff. To me that doesn't necessarily fit in any of the abstraction levels, as IMHO the source generation context should be carried with the type, not the options. Serde solves the association problem by directly providing the source generation into the user type by implementing an appropriate interface. I wrote up some details in the docs on how that works, and how it can generate wrappers for types which aren't under the user's control: https://commentout.com/serde-dn/generator.html |
Cleanest solution for now. Still not great to have to pass it in every serialization. Id rather have the risk of not configuring it when needed than to add the options everytime.... |
Per previous discussions in this thread, we don't plan on making At the same time, the conversation has spun into a discussion about potentially exposing the serialization APIs as instance methods: I've opened a separate issue (#74492) to track this, feel free to contribute to the discussion there. |
@eiriktsarpalis Nobody would have wanted to change the default unless you guys chose some completely irrational value as default. With the current default, it is almost impossible to use the JsonSerializer without changing the current default values. Without any second thought following two should have been defaulted in the library: PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, If your default is needed to change every time, then your default is entirely wrong and useless. You can ask the whole world (except dotNET Team), and almost everybody will say that JSON deserialization must be case insensitive and serialization should be in camelCase by default. What's wrong with changing the above two defaults in the library as these would not break any existing thing? |
New API:
Newtonsoft \ JSON.NET does it this way:
The upside is that you can have different settings for different situations, including different options per assembly, even though that would require the developer be aware of the potential pitfalls of that. The fact that it has also worked for many years in JSON.NET I think is a plus as well.
Performance could suffer a little bit, would have to test. Either way, one could always get over that by explicitly passing their options.
Original text:
Provide a way to change the default settings for the static
JsonSerializer
class. In Json.NET, you could do:Currently you have to keep passing the options around and remembering to pass it to each call to
[De]Serialize
The text was updated successfully, but these errors were encountered: