Repository navigation
Let a null be the result of a transformation (breaking) #2481
Copy link
Copy link
Open
Labels
Description
Activity
Some items of "To decide along the way" now have their own issues. These issues do not need the breaking change:
- A
nulldictionary value: Gremlinq keeps it when the value type can holdnull, for a plain object and for ag:Map. The dictionary converter decides this itself, as the array converters do for anullitem. See Newtonsoft drops a null value from a g:Map #2492 (N) and Gremlinq/Gremlinq.Extensions#230 (STJ). Property<T>andVertexProperty<T>with anullvalue: the converters decline the token and do not throw. See Newtonsoft throws for some input that it cannot read #2486 (N, Core) and Gremlinq/Gremlinq.Extensions#234 (STJ). This issue can still decide later to acceptnullas a value.
A decision about the last row of the table ("each implementation's own token"):
- A Newtonsoft token (
JValue,JObject) can be the result when it can stand in for the requested type. Callers use these tokens, and they stay usable. A change would break these callers. - A
JsonElementmust never be the result. It becomes unusable when the memory of itsJsonDocumentis released. See Let a serializer stop the transformer from giving its own token as the result #2495 (Core) and Gremlinq/Gremlinq.Extensions#229 (STJ).
- A
Update after #2486, #2492 and Gremlinq/Gremlinq.Extensions#230, #234 landed (2026-10-09). The table under "Today" has changed in these rows, for both implementations:
Input Answer now { "key": "k", "value": null }asProperty<string>; the same forVertexProperty<string>declined; nothing throws { "a": "x", "b": null }asDictionary<string, string>, as a plain object and as ag:Mapb: nullis kept{ "a": 1, "b": null }asDictionary<string, int>the entry bis left out; nothing throwsa nullvalue in a plain object or ag:Mapread asDictionary<string, object>,IDictionaryorobjecta real null, noJValueand noJsonElementTwo corrections to the comment above:
- Newtonsoft throws for some input that it cannot read #2486 did not change Core. The constructor of
Property<T>still throws fornull; the converters of both implementations decline before they call it. If this issue decides to acceptnullas a property value, it has to change that constructor and thePropertyConverterFactoryof both implementations. - The decision about the last row is replaced: no native token may be a result, a Newtonsoft
JTokenno more than aJsonElement. For Newtonsoft this is a breaking change and part of Let a serializer stop the transformer from giving its own token as the result #2495, next major release.
- Newtonsoft throws for some input that it cannot read #2486 did not change Core. The constructor of
A transformation cannot answer with
nulltoday. This issue proposes to let it, which is a breaking change toExRam.Gremlinq.Core's public contract.Today
ITransformer.TryTransformandIConverter.TryConvertboth declare[NotNullWhen(true)] out TTarget? value: success promises a value that is not null.Transformerenforces it. A converter's answer only counts whenTryConvert(...) is true && optionValue is not null(src/Core/Transformation/Transformer.cs). A converter that answers "success, null" is treated as if it had declined, and the next converter is asked.GraphsonSupportTestBase.Nullable_null_at_top_leveldocuments this forint?: at the top level there is nothing to hold a null,TryTransformcannot report one, and a successful null is discarded, "being indistinguishable from a decline".What follows from it
Measured on both
Support.NewtonsoftJsonandSupport.SystemTextJson(Gremlinq.Extensions), on14.xplus #2477 and Gremlinq/Gremlinq.Extensions#214:nullasstring,Uri,Person,int?,object[], a dictionaryTransformTo<T>().From(...)throwsInvalidCastException[ "a", null ]asstring[][ a, null ]. The array decides for itself and keeps the nullnull{ "key": "k", "value": null }asProperty<string>; the same forVertexProperty<string>ArgumentNullExceptionfrom theProperty<T>constructor{ "a": "x", "b": null }asDictionary<string, string>b: null; System.Text.Json drops the entry. As ag:Map, both drop it{ "a": 1, "b": null }asDictionary<string, int>ArgumentNullException; System.Text.Json drops the entrynullinside a map or object that is read asobject;{ "@type": "g:Int32", "@value": null }asobjectnull, but each implementation's own token: aJValuefrom Newtonsoft, aJsonElementfrom System.Text.JsonThe last row is the clearest symptom. Because
nullcannot be a result, the chain of converters falls through to the fallback that hands back the token it was given.#2477 pins the first three rows as today's contract. The
Property<T>and dictionary rows are not pinned.Proposal
Let
nullbe a legitimate result of a transformation:TryTransformandTryConvertmay answertruewith anullvalue when the requested type can hold one (a reference type orNullable<T>). The[NotNullWhen(true)]annotations go.Transformerno longer discards a successfulnull.nullrequested as a type that can hold it is answered withnull, at the top level and inside structures alike. A type that cannot hold it (int) still declines.What breaks
valueaftertrueget nullable warnings, andTransformTo<T>().From(...)can returnnullfor aTthat is not annotated as nullable. This needs a major version.Transformermoves on to the next converter;NullableConverterdoes it. Afterwards the first such answer ends the chain. Every converter in both implementations has to be checked for it.nullresult:String_from_null,Uri_from_null,Person_from_null,String_from_single_item_array_with_null,String_from_typed_value_with_null,String_from_Property_with_null_value,String_from_VertexProperty_with_null_value,Property_of_string_from_null,VertexProperty_of_string_from_null, andNullable_null_at_top_level.To decide along the way
Property<T>andVertexProperty<T>accept anullvalue, or a property whose value isnullis declined. Their constructors throw onnulltoday.nullvalue: keep the entry, in both implementations and for both plain objects andg:Map.nullin the JSON is assignednullor left at its initializer. The two are only different for a member with a non-null initializer.Both implementations have to change together:
Support.NewtonsoftJsonhere, andSupport.SystemTextJsonin Gremlinq.Extensions.