-
-
Notifications
You must be signed in to change notification settings - Fork 159
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
Use System.Text.Json #1075
Merged
Merged
Use System.Text.Json #1075
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Codecov Report
@@ Coverage Diff @@
## master #1075 +/- ##
==========================================
+ Coverage 88.48% 88.66% +0.18%
==========================================
Files 256 248 -8
Lines 7024 7086 +62
==========================================
+ Hits 6215 6283 +68
+ Misses 809 803 -6
Continue to review full report at Codecov.
|
446034b
to
572a521
Compare
…en not found; added TryGetResourceContext() that returns null
Note we need JsonDateTimeOffsetFormatSpecifier now, because STJ never tries to infer the CLR type from JSON values between quotes, while Newtonsoft does. So Newtonsoft would convert both values to date/time, effectively hiding the textual difference that was always there.
…erly handles self-referencing EF Core objects when enabling reference tracking, as opposed to Newtonsoft.
…ns into account, which is unneeded because we only care about whether there's a diff, not so much what that diff looks like. And we don't expect self-references here (it would have crashed in the past, and will now too).
…sts, because we already use indenting in TestableStartup, so this is no longer needed.
572a521
to
0487b73
Compare
bca9e8b
to
85e2abe
Compare
0fb80f0
to
070c98b
Compare
Bugfix: jsonapi/version was missing in error responses
… what Ember.js expects it to be named (see https://guides.emberjs.com/release/models/handling-metadata/)
…defaults for tests, because that hides the problem when we forget to put it on a member that needs it.
Bugfix: do not break out of method on first attribute
070c98b
to
16ec48a
Compare
- Added various converters to steer JsonSerializer in the right direction - JsonApiDotNetCore.Serialization.Objects - Removed inheritance in JsonApiDotNetCore.Serialization.Objects, so we're in control of element write order - Moved "meta" to the end in all types (it is secondary information) - Consistently set IgnoreCondition on all properties, so we don't need to override global options anymore
…ws sample data on F5.
b554f05
to
705e8d5
Compare
By default, this produces: ``` The JSON value could not be converted to JsonApiDotNetCore.Serialization.Objects.SingleOrManyData`1[JsonApiDotNetCore.Serialization.Objects.ResourceObject]. Path: $.data | LineNumber: 3 | BytePositionInLine: 11. ``` which is totally unhelpful. Because this is so likely to hit users, we special-case here to produce a better error.
4a7f8d7
to
32f3af0
Compare
6011cb5
to
e655fa6
Compare
594b533
to
2e0c811
Compare
maurei
approved these changes
Sep 17, 2021
bart-degreed
pushed a commit
that referenced
this pull request
Sep 20, 2021
* Removed temporary workaround to start postgres service in cibuild (#1083) * Restored docs for HasManyThrough, which was removed in #1037 (#1084) * Use System.Text.Json (#1075) * Removed Serialization.Client.Internal * Removed undocumented ?nulls and ?defaults query string support * Refactor: use interpolated strings instead of concatenation * Updated tests to use string value for IDs; centralized pseudo-constants * Added tests for pascal casing * Optimized attribute/relationship lookups * Breaking: Made IResourceContextProvider.GetResourceContext() throw when not found; added TryGetResourceContext() that returns null * Optimized resource graph lookups * Breaking: Merged IResourceContextProvider into IResourceGraph * Switched to STJ in assertions Note we need JsonDateTimeOffsetFormatSpecifier now, because STJ never tries to infer the CLR type from JSON values between quotes, while Newtonsoft does. So Newtonsoft would convert both values to date/time, effectively hiding the textual difference that was always there. * Switched to STJ in rendering exception stack traces * Switched to STJ in rendering CLR objects as part of tracing. STJ properly handles self-referencing EF Core objects when enabling reference tracking, as opposed to Newtonsoft. * Switched to STJ in attribute change tracking. This used to take options into account, which is unneeded because we only care about whether there's a diff, not so much what that diff looks like. And we don't expect self-references here (it would have crashed in the past, and will now too). * Switched to STJ in Microservices example * Removed re-indent of response body on HTTP status code mismatch in tests, because we already use indenting in TestableStartup, so this is no longer needed. * Use STJ naming convention on special-cased code paths * Renamed RelationshipEntry to RelationshipObject, Error to ErrorObject * Fix broken test in cibuild * Fixed broken tests in cibuild due to different line endings * Package updates * Refactor serialization objects - Simplified error objects, so they are similar to the other serialization objects. This means no default instances, constructors (exception: ErrorObject) or conditional serialization logic. And explicit names to overrule naming conventions. And annotations to skip serialization when null. - Added missing members from JSON:API v1.1 spec: ErrorDocument.Meta, ErrorLinks.Type, ErrorSource.Header, ResourceIdentifierObject.Meta - Normalized collection types - Updated documentation: Link to v1.1 of JSON:API spec instead of copy/pasted text * Merged ErrorDocument and AtomicOperationsDocument into Document Bugfix: jsonapi/version was missing in error responses * Fill error.source.header where applicable * Breaking: Renamed "total-resources" meta key to "total" because thats what Ember.js expects it to be named (see https://guides.emberjs.com/release/models/handling-metadata/) * Removed unneeded StringEnumConverter usage. Also removed it from the defaults for tests, because that hides the problem when we forget to put it on a member that needs it. * Use configured STJ options for null/default value inclusion Bugfix: do not break out of method on first attribute * Fixed data type in json request body * Added missing type, which is a required element * Converted core code to use System.Text.Json - Added various converters to steer JsonSerializer in the right direction - JsonApiDotNetCore.Serialization.Objects - Removed inheritance in JsonApiDotNetCore.Serialization.Objects, so we're in control of element write order - Moved "meta" to the end in all types (it is secondary information) - Consistently set IgnoreCondition on all properties, so we don't need to override global options anymore * Updated documentation * Fixed broken example-generation. Set launchBrowser to true, so it shows sample data on F5. * Inlined properties on serializable objects * Add test for incompatible ID value. By default, this produces: ``` The JSON value could not be converted to JsonApiDotNetCore.Serialization.Objects.SingleOrManyData`1[JsonApiDotNetCore.Serialization.Objects.ResourceObject]. Path: $.data | LineNumber: 3 | BytePositionInLine: 11. ``` which is totally unhelpful. Because this is so likely to hit users, we special-case here to produce a better error. * Removed misplaced launchsettings.json * Review feedback: use base class instead of static helper * Update ROADMAP.md * Post-merge fixes * Replace NewtonsoftDataContractResolver with JsonSerializerDataContractResolver * Small cleanups Co-authored-by: maurei <maurits.moeys@gmail.com>
5 tasks
bart-degreed
pushed a commit
that referenced
this pull request
Dec 3, 2021
* Removed Serialization.Client.Internal * Removed undocumented ?nulls and ?defaults query string support * Refactor: use interpolated strings instead of concatenation * Updated tests to use string value for IDs; centralized pseudo-constants * Added tests for pascal casing * Optimized attribute/relationship lookups * Breaking: Made IResourceContextProvider.GetResourceContext() throw when not found; added TryGetResourceContext() that returns null * Optimized resource graph lookups * Breaking: Merged IResourceContextProvider into IResourceGraph * Switched to STJ in assertions Note we need JsonDateTimeOffsetFormatSpecifier now, because STJ never tries to infer the CLR type from JSON values between quotes, while Newtonsoft does. So Newtonsoft would convert both values to date/time, effectively hiding the textual difference that was always there. * Switched to STJ in rendering exception stack traces * Switched to STJ in rendering CLR objects as part of tracing. STJ properly handles self-referencing EF Core objects when enabling reference tracking, as opposed to Newtonsoft. * Switched to STJ in attribute change tracking. This used to take options into account, which is unneeded because we only care about whether there's a diff, not so much what that diff looks like. And we don't expect self-references here (it would have crashed in the past, and will now too). * Switched to STJ in Microservices example * Removed re-indent of response body on HTTP status code mismatch in tests, because we already use indenting in TestableStartup, so this is no longer needed. * Use STJ naming convention on special-cased code paths * Renamed RelationshipEntry to RelationshipObject, Error to ErrorObject * Fix broken test in cibuild * Fixed broken tests in cibuild due to different line endings * Package updates * Refactor serialization objects - Simplified error objects, so they are similar to the other serialization objects. This means no default instances, constructors (exception: ErrorObject) or conditional serialization logic. And explicit names to overrule naming conventions. And annotations to skip serialization when null. - Added missing members from JSON:API v1.1 spec: ErrorDocument.Meta, ErrorLinks.Type, ErrorSource.Header, ResourceIdentifierObject.Meta - Normalized collection types - Updated documentation: Link to v1.1 of JSON:API spec instead of copy/pasted text * Merged ErrorDocument and AtomicOperationsDocument into Document Bugfix: jsonapi/version was missing in error responses * Fill error.source.header where applicable * Breaking: Renamed "total-resources" meta key to "total" because thats what Ember.js expects it to be named (see https://guides.emberjs.com/release/models/handling-metadata/) * Removed unneeded StringEnumConverter usage. Also removed it from the defaults for tests, because that hides the problem when we forget to put it on a member that needs it. * Use configured STJ options for null/default value inclusion Bugfix: do not break out of method on first attribute * Fixed data type in json request body * Added missing type, which is a required element * Converted core code to use System.Text.Json - Added various converters to steer JsonSerializer in the right direction - JsonApiDotNetCore.Serialization.Objects - Removed inheritance in JsonApiDotNetCore.Serialization.Objects, so we're in control of element write order - Moved "meta" to the end in all types (it is secondary information) - Consistently set IgnoreCondition on all properties, so we don't need to override global options anymore * Updated documentation * Fixed broken example-generation. Set launchBrowser to true, so it shows sample data on F5. * Inlined properties on serializable objects * Add test for incompatible ID value. By default, this produces: ``` The JSON value could not be converted to JsonApiDotNetCore.Serialization.Objects.SingleOrManyData`1[JsonApiDotNetCore.Serialization.Objects.ResourceObject]. Path: $.data | LineNumber: 3 | BytePositionInLine: 11. ``` which is totally unhelpful. Because this is so likely to hit users, we special-case here to produce a better error. * Removed misplaced launchsettings.json * Review feedback: use base class instead of static helper
bart-degreed
pushed a commit
that referenced
this pull request
Dec 3, 2021
* Removed Serialization.Client.Internal * Removed undocumented ?nulls and ?defaults query string support * Refactor: use interpolated strings instead of concatenation * Updated tests to use string value for IDs; centralized pseudo-constants * Added tests for pascal casing * Optimized attribute/relationship lookups * Breaking: Made IResourceContextProvider.GetResourceContext() throw when not found; added TryGetResourceContext() that returns null * Optimized resource graph lookups * Breaking: Merged IResourceContextProvider into IResourceGraph * Switched to STJ in assertions Note we need JsonDateTimeOffsetFormatSpecifier now, because STJ never tries to infer the CLR type from JSON values between quotes, while Newtonsoft does. So Newtonsoft would convert both values to date/time, effectively hiding the textual difference that was always there. * Switched to STJ in rendering exception stack traces * Switched to STJ in rendering CLR objects as part of tracing. STJ properly handles self-referencing EF Core objects when enabling reference tracking, as opposed to Newtonsoft. * Switched to STJ in attribute change tracking. This used to take options into account, which is unneeded because we only care about whether there's a diff, not so much what that diff looks like. And we don't expect self-references here (it would have crashed in the past, and will now too). * Switched to STJ in Microservices example * Removed re-indent of response body on HTTP status code mismatch in tests, because we already use indenting in TestableStartup, so this is no longer needed. * Use STJ naming convention on special-cased code paths * Renamed RelationshipEntry to RelationshipObject, Error to ErrorObject * Fix broken test in cibuild * Fixed broken tests in cibuild due to different line endings * Package updates * Refactor serialization objects - Simplified error objects, so they are similar to the other serialization objects. This means no default instances, constructors (exception: ErrorObject) or conditional serialization logic. And explicit names to overrule naming conventions. And annotations to skip serialization when null. - Added missing members from JSON:API v1.1 spec: ErrorDocument.Meta, ErrorLinks.Type, ErrorSource.Header, ResourceIdentifierObject.Meta - Normalized collection types - Updated documentation: Link to v1.1 of JSON:API spec instead of copy/pasted text * Merged ErrorDocument and AtomicOperationsDocument into Document Bugfix: jsonapi/version was missing in error responses * Fill error.source.header where applicable * Breaking: Renamed "total-resources" meta key to "total" because thats what Ember.js expects it to be named (see https://guides.emberjs.com/release/models/handling-metadata/) * Removed unneeded StringEnumConverter usage. Also removed it from the defaults for tests, because that hides the problem when we forget to put it on a member that needs it. * Use configured STJ options for null/default value inclusion Bugfix: do not break out of method on first attribute * Fixed data type in json request body * Added missing type, which is a required element * Converted core code to use System.Text.Json - Added various converters to steer JsonSerializer in the right direction - JsonApiDotNetCore.Serialization.Objects - Removed inheritance in JsonApiDotNetCore.Serialization.Objects, so we're in control of element write order - Moved "meta" to the end in all types (it is secondary information) - Consistently set IgnoreCondition on all properties, so we don't need to override global options anymore * Updated documentation * Fixed broken example-generation. Set launchBrowser to true, so it shows sample data on F5. * Inlined properties on serializable objects * Add test for incompatible ID value. By default, this produces: ``` The JSON value could not be converted to JsonApiDotNetCore.Serialization.Objects.SingleOrManyData`1[JsonApiDotNetCore.Serialization.Objects.ResourceObject]. Path: $.data | LineNumber: 3 | BytePositionInLine: 11. ``` which is totally unhelpful. Because this is so likely to hit users, we special-case here to produce a better error. * Removed misplaced launchsettings.json * Review feedback: use base class instead of static helper
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR replaces the Newtonsoft.Json serialization library used by JADNC with System.Text.Json (STJ).
Fixes #664.
Fixes #999.
Fixes #1077.
Fixes #1078.
Aside from the actual conversion, this PR also contains several prerequisite refactorings and bug fixes, which I tried to put in the preceding commits.
Refactorings
null
, which should not crash, so added integration tests for pascal casing.IResourceContextProvider.GetResourceContext()
throw when not found; addedTryGetResourceContext()
that returns null when not found.IResourceContextProvider
intoIResourceGraph
JsonApiDotNetCore.Serialization.Objects
namespace. These objects represent the low-level JSON:API object shape. While technically a breaking change, we don't expect anyone to depend on these directly.RelationshipEntry
toRelationshipObject
,Error
toErrorObject
ErrorDocument
andAtomicOperationsDocument
intoDocument
. An observable change is that we used to putmeta
at the top for successful responses, but at the bottom for error responses. We now always putmeta
at the end. The JSON:API spec leaves room for different interpretations aboutmeta
order in the different object types, so this is a judgment call.meta
is typically used for comments and secondary information, which we believe should not be transmitted before the more important information. We moved thejsonapi
object to the top, which enables clients to vary parsing the rest of the body, based on the JSON:API version and extensions.ErrorObject
) or conditional serialization logic. And explicit names to overrule naming conventions. And annotations to skip serialization when null.ErrorDocument.Meta
,ErrorLinks.Type
,ErrorSource.Header
,ResourceIdentifierObject.Meta
IgnoreCondition
(the equivalent of NewtonsoftsNullValueHandling
) on all properties, so we don't need to override global options anymore.error.source.header
in error response forAccept
,Content-Type
andIf-Match
headers.JsonStringEnumConverter
to the list of converters in global options in tests anymore, because it hides the problem when we forget to explicitly put it on a serialized object.Conversion to STJ
Switching to STJ enabled us to remove some workarounds for async calls and oddities around date/time and floating-point numbers handling, but also introduced several limitations. For low-level reading and writing, STJ provides
Utf8JsonReader
andUtf8JsonWriter
. They have methods likeReadStartObject
,ReadInt32
,WriteString
,WriteEndObject
etc. At a higher level, there'sJsonSerializer
, a serializer/deserializer for object graphs. It heavily delegates toJsonConverter
instances to process parts of the tree. In the endless list of unsupported features compared to Newtonsoft, the answer is almost always "create a custom converter". This sounds doable at first, but it turns out the position reporting for errors becomes invalid when custom converters come into play. Over time, something has gone terribly wrong! As explained here, converters were never designed to be used like that. There exist two kinds of converters: the public ones for primitive values (the leaves in a JSON tree:int
,string
,DateTime
etc.) and the internal ones for intermediate nodes, such asList
,Dictionary
,Object
etc. These internal converters have their own internal location tracking (the position in the text stream, as well as the depth and the JSON path, such as$.data[0].attributes.firstName
) using an internalReadStack
type. Now as long as the whole tree is read by internal converters, they pass along theirReadStack
and everything is fine. But when a custom converter is invoked somewhere in the middle of the tree, theReadStack
cannot be passed along and any subsequent internal converters can no longer access it. The reported position is then what theReadStack
contains, which is only a part of the path, so incorrect. The lack of correct position information, combined with the terrible error messages is a big downside that we cannot solve (other than discarding the wholeJsonSerializer
/Converter
model and using onlyUtf8JsonReader
, which we quickly discarded as an option). The best we can do here is first try to minimize the use of custom converters. A converter cannot be read-only or write-only, so we keep separate settings for reading and writing internally, which are copies of the basic settings, but with different converters added to them. (Note: although the trick of cloning options works (see #664 (comment)), it makes the serializer 100x slower compared to Newtonsoft, so a no-go.) Another way to reduce the pain is by not throwing from a converter when unable to read due to an unknown resource type, an invalid attribute value, etc. So instead we put a sentinel value in the tree, which we'll detect in a post-processing phase (there we know the location in the tree), and throw from there.Reading the JSON:API
data
element, which can benull
, an object, or an array of objects requires the use of a custom converter. Likewise, the JSON:API dictates some rules when to write "data:null" vs when to omit it entirely, which can only be solved using custom converters. Instead of a base class, this is now areadonly struct
(we never want it to benull
), which has anobject Value
property, along withSingleData
/ManyData
calculated properties.Another challenge is that
type
may occur afterattributes
in a JSON body. But we need to know thetype
and find it in the resource graph before we can convert the attribute values to CLR objects. Therefore we read the subtree twice: in the first pass, we extract the type, then in the second pass we handle everything else.We found that the built-in
JsonStringEnumConverter
from STJ has various problems, so we use the one from Macross in our tests. For the library itself, the issues don't apply. Likewise, we use code from https://github.com/J0rgeSerran0/JsonNamingPolicy to support casing conventions other than the built-in pascal case and camel case. Same story forTimeSpan
, which is completely unsupported until .NET 6.Conclusion
I've gone back and forth several times on aborting the whole STJ effort. Initially, I hoped to get rid of most post-processing and instead validate in-place using converters. But the limited built-in validation capabilities, combined with the useless error reporting make that a no-go, so we'll still need heavy post-processing. What's worse, in many bug reports and feature requests on GitHub, the owners are hesitant to address them, saying: "we'll wait to see if more people are asking for this". But they have locked all conversations, which makes it impossible to even upvote! I explored if moving to .NET 6 would take away some pain, but it won't and developers are very upset about it (https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/#comment-9821, dotnet/runtime#31619 (comment)). During my investigations, I've read numerous users reporting to cancel their migration efforts. I sincerely hope the many problems will be addressed soon. They killed Newtonsoft innovation and replaced it with something that's full of bugs and limitations.
On the plus side, STJ eliminates the NuGet package dependency hell around conflicting Newtonsoft versions.
Performance
The snapshots below were taken using #1023 on my Windows 10 laptop running Kestrel on a Release build (without the debugger attached) of the JsonApiDotNetCoreExample project with a small amount of seed data in a PostgreSQL database. Both libraries are slow on first calls because they initialize their caches, so I've done multiple requests and picked one that looks representative. The percentages are what they amount to in the entire request duration, excluding the time spent in database calls.
QUALITY CHECKLIST