-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Sample to customize what STJ considers a 'JSON null'/Ignoring property writing #82530
Comments
Ignore conditions are generally specified on the property level and not the type level. Converters are not the appropriate abstraction to configure this -- instead this is done globally using the Note that type Data =
{
Property: string voption
}
let options = JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)
JsonSerializer.Serialize({ Property = ValueNone }, options) |> printfn "%s" // {} |
Let me add another constraint, we've been going in circles for years on this and it seems like there still isn't a decent way to do this in STJ. type Data =
{
Property: string voption
Number: int
}
let options = JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)
JsonSerializer.Serialize({ Property = ValueNone, Number = 1 }, options) |> printfn "%s" // expect { "Number": 1 }
JsonSerializer.Serialize({ Property = ValueNone; Number = 0 }, options) |> printfn "%s" // expect { "Number": 0 }
I don't want all default values to disappear, this is not friendly for untyped clients to deal with. I do want any json nulls to cause properties to be omitted. I want some way to mark a converter as a 'nullable-ish' handler. ValueOption is a struct that has a specific value for null which should cause WhenWritingNull semantics to kick in for that particular value, just like it does for System.Nullable which is also not a reference type.
Nullable and WhenWritingNull: type Data =
{
NullableInt: Nullable<int>
}
[<EntryPoint>]
let main args =
let options = JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)
JsonSerializer.Serialize({ NullableInt = Nullable() }, options) |> printfn "%s" // {} |
Maybe, but that ship has sailed since the feature got shipped years ago. If you don't want that behaviour, apply it on a per-property basis using an attribute. If that is too involved, write a contract resolver that applies a
Why do you think it is inconsistent? Doesn't System.Nullable allow
We made the determination that this extensibility should live at the JsonPropertyInfo level and not converter level. Are there any specific issues with |
Yes we can get into the technicalities here (and believe me I understand the nuances) but the xmldoc for What was designed incorrectly IMO in the serializer is the choice to equate only CLR nulls to json nulls. This was not done for any of the other token types like numbers, arrays, objects etc. You write a custom converter to make STJ respect the converted type's particularities by taking control over its handling. For a converter during serialization what matters is the output, so naturally you want your converter be able to take into account its json null behavior as well, not just when they happen to be CLR nulls. (just like we can on the deserialization side!) If that would have been the approach then naturally it would fall out that there should be an api on converters to tell the serializer what values in the converted type's domain map to a json null. Either as an equality predicate or as a configurable set of values. The STJ assumption that CLR null and only CLR null = json null is bolted into the serializer and that is precisely what I want to be able to customize.
Most prominently the boxing, why would somebody use ValueOption or Skippable if it gets boxed at the edges due to a STJ mandated callback anyway... (exactly the same would be true for System.Nullable if it didn't have builtin support) I strongly believe that to properly deal with mapping structs to json there needs to be extensibility in determining which of its values map to json null values. This mapping is a type level detail and should live with the other type level conversion logic in the converter. |
I think this is an issue of API documentation being incomplete. We should update it to say that it applies to nullable types and not reference types specifically. The source for truth for API documentation is the dotnet-api-docs repo.
As designed, the ignore feature makes a determination on whether a property should be skipped based on the .NET value it contains. It does not apply to root values and it does not apply to collection elements. I wouldn't agree that it's incorrect, if anything making an "ignore" judgement based on what JSON tokens a converter wrote on the wire definitely sounds like putting the cart before the horse.
I get your point, however this would need to be new virtual method added to the converter type and not something that could be articulated in Because ignoring only really applies to properties, and ignore predicates are scoped to individual properties and not types, we made the conscious decision to expose the predicate on
As already mentioned, we are planning on addressing this in the future via a strongly typed |
I'll open an issue, maybe even a PR.
I understand this is the design but this doesn't have much to do with what CLR values trigger this property ignore behavior. Where - just on properties - the behavior applies is OK as far as I'm concerned. It's CLR centric as it does not apply to ExtensionData 'properties' for instance (I'm not fully happy with this but that's ok, you have a serviceable workaround of removing the key in such cases). When a user wants more ad-hoc ignore behaviors/predicates that is indeed something that belongs in the contract. We're aligned on this so far. However 'when' (i.e. which values apply to this behavior) should IMO be expanded to respect some configurable nullability on a converter.
Correct, I was not implying that it should be rolled into Write. Maybe we can even come to a compromise and instead of having a full blown predicate api like
We have numerous api types where over 70% of their properties are specified as some instantiation of ValueOption/Skippable. Our most extreme use of Skippable would be our patch endpoints where effectively every property is optional. Though on other endpoints there are quite some as well, either for properties we apply a default for if not provided (think ids or flags) or where it really just is optional data.
As you know any heap allocation always has some impact, it's just a question of how much total machine throughput you're fine with sacrificing. This is a general truth which doesn't need minutia benchmarks to be performed before it can be applied. Completely agreed that there is value in having a benchmark to evaluate the effectiveness of moving to strongly typed predicates though as that implies more than just eliding a box. |
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis Issue DetailsThe following issue was closed #55781 (comment) with the mention this is solved by contract resolvers, I'm just not sure how this works (and ideally without boxing). I'd be curious to the solution/sample to solve the following repro: Given the following converter and code: type JsonValueOptionConverter<'T>(converter: JsonConverter) =
inherit JsonConverter<ValueOption<'T>>()
let valueTy = typeof<'T>
let valueConverter = converter :?> JsonConverter<'T>
override _.Read(reader, _, options) =
match reader.TokenType with
| JsonTokenType.Null -> ValueNone
| _ -> ValueSome(valueConverter.Read(&reader, valueTy, options))
override _.Write(writer, value, options) =
match value with
| ValueNone ->
// TODO revisit after https://github.com/dotnet/runtime/issues/33433
// and https://github.com/dotnet/runtime/issues/50294
writer.WriteNullValue()
| ValueSome x -> valueConverter.Write(writer, x, options)
type JsonValueOptionConverter() =
inherit JsonConverterFactory()
static let valueTyDef = typedefof<ValueOption<_>>
static let converterTyDef = typedefof<JsonValueOptionConverter<_>>
override _.CanConvert(ty) = ty.IsConstructedGenericType && valueTyDef = (ty.GetGenericTypeDefinition())
override _.CreateConverter(ty, options) = Activator.CreateInstance(converterTyDef.MakeGenericType(ty.GenericTypeArguments.[0]), options.GetConverter(ty.GenericTypeArguments.[0])) :?> JsonConverter
type Data =
{
Property: string voption
}
let options =
let options = JsonSerializerOptions()
options.Converters.Add(JsonValueOptionConverter())
options.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull // something here??
// Expect:
JsonSerializer.Serialize({ Property = None }, options) = "{}"
// Actual:
JsonSerializer.Serialize({ Property = None }, options) = "{ "Property": null }" How would I get to the expected output app wide?
|
I'll point out that "IsValueJsonNull" is not the same as "SkipIfIsValueJsonNull". I don't think there could be any general-purpose converter that unconditionally signals that the JSON nulls it produces should be skipped if placed on a property. At the same time, I don't think that property skippability is inherently tied to the .NET value being Generally speaking I find that skippability is context-specific, depending on public class JsonConverter<T>
{
public virtual void OnPropertyInfoCreated(JsonPropertyInfo propertyInfo);
} which can be used as follows: type JsonValueOptionConverter<'T>(converter: JsonConverter) =
inherit JsonConverter<'T voption>()
override _.OnPropertyInfoCreated(JsonPropertyInfo propertyInfo) =
propertyInfo.ShouldSerialize <- new Func<obj,obj,_>(fun _ value -> match value :?> 'T voption with ValueSome _ -> true | _ -> false) But I don't see much value of such a gadget over just using contract customization directly. The semantics also become a bit hairy once you take into account that converters can be configured on a per-property basis using the Another possibility is that we regard rendering of public struct JsonWriterOptions
{
public bool SkipNullProperties { get; set; } = false;
public bool SkipNullArrayElements { get; set; } = false;
} |
This has been my argument for a long time. See also #68128 (and all associated and linked issues). |
Skipping is indeed a property level concern. But to reiterate: when to skip is hardcoded or immediately so flexible it brings boxing overhead making all current approaches unfit. Today we have these broad categories for skipping on write:
My proposal really comes down to allowing converters to redefine their 'null' value. Unconfigured this will just be a CLR null (or nothing if its a struct type, just like today) but configured it could point to
As you see it is decoupled from skipping entirely. Skipping just taps into this to understand when to skip if WhenWritingNull is specified. IMO decoupling this has the added benefit that when there are other concerns that want to understand a type's nullability mapping they can do so. The values which should be treated as null are a type invariant. All of these could even be designed to throw if they're supplied for reference types in my opinion, I really just care about the structs here. My ideal mechanism would be:
The writer solution you proposed is an example that probably won't work because I assume it would require entire properties to be buffered when this mode is enabled, or a full second pass to 'format'. There is no problem with the writer as is, it's the serializer which should check the precondition of some form of 'should this property be serialized' after all.
I had some difficulty grasping the nuance you tried to add here. Just to be sure, I'm not suggesting the serializer should in one or the other form 'inspect' what calls the converter makes/made to the writer to determine whether it should ignore anything or not. |
I'm not sure what that means. Is it effectively a way for the converter to declare that it will be deserializing the particular values as JSON What's more, it would be a breaking change. When designing the contract model, we realized that there are limits to what can be expressed with Lines 344 to 387 in 2bdc3cb
I think your best bet might be to wait for the strongly-typed |
Correct it would be a way to configure WhenWritingNull to allow converters to point to a null value for struct types. I understand it's probably undesirable but it shouldn't be a breaking change for user converters given they would need to opt into overriding the new member. For the converters that come with STJ I see how that may be a different story when people already use WhenWritingNull and ValueOption and have tests to check their output. However as ValueOption is semantically the same as Option and one ends up with one result, a property with null, while the other has no property it could be argued it's ok to break to bring consistency, but that's not up to me.
I assumed as much. It doesn't help much at this moment without access to the generic form though. |
I don't think controlling this behaviour on the converter level is viable given the current shipped behavior. You would need to rely on the property contract model instead. I've created #82720 since apparently we hadn't created a tracking issue specifically for generic |
The following issue was closed #55781 (comment) with the mention this is solved by contract resolvers, I'm just not sure how this works (and ideally without boxing).
I'd be curious to the solution/sample to solve the following repro:
Given the following converter and code:
How would I get to the expected output app wide?
@eiriktsarpalis
The text was updated successfully, but these errors were encountered: