From f50b54b3f6195a23f71d98c7115fc2619dd55756 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 8 Aug 2021 13:35:41 -0400 Subject: [PATCH] New: Use System.Text.Json for Nancy and SignalR Co-Authored-By: ta264 --- .../Serializer/Newtonsoft.Json/Json.cs | 59 ++++++++++++++++--- .../Qualities/QualityProfileQualityItem.cs | 4 +- src/NzbDrone.Core/Qualities/QualityModel.cs | 2 +- .../ThingiProvider/ProviderRepository.cs | 3 +- src/NzbDrone.Host/Sonarr.Host.csproj | 1 - .../WebHost/WebHostController.cs | 4 +- src/NzbDrone.SignalR/SignalRMessage.cs | 3 +- src/Sonarr.Api.V3/Commands/CommandResource.cs | 2 +- .../CustomFilters/CustomFilterResource.cs | 27 +++++---- src/Sonarr.Api.V3/Indexers/ReleaseResource.cs | 10 +--- src/Sonarr.Api.V3/Sonarr.Api.V3.csproj | 1 - src/Sonarr.Api.V3/Update/UpdateResource.cs | 2 - src/Sonarr.Http/ClientSchema/SchemaBuilder.cs | 24 ++++++-- .../Extensions/NancyJsonSerializer.cs | 10 +++- .../Extensions/ReqResExtensions.cs | 4 +- src/Sonarr.Http/REST/RestModule.cs | 13 ++-- src/Sonarr.Http/REST/RestResource.cs | 6 +- src/Sonarr.Http/Sonarr.Http.csproj | 1 - 18 files changed, 114 insertions(+), 62 deletions(-) diff --git a/src/NzbDrone.Common/Serializer/Newtonsoft.Json/Json.cs b/src/NzbDrone.Common/Serializer/Newtonsoft.Json/Json.cs index 47d5bce67..fce2f295f 100644 --- a/src/NzbDrone.Common/Serializer/Newtonsoft.Json/Json.cs +++ b/src/NzbDrone.Common/Serializer/Newtonsoft.Json/Json.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; @@ -38,12 +39,61 @@ namespace NzbDrone.Common.Serializer public static T Deserialize(string json) where T : new() { - return JsonConvert.DeserializeObject(json, SerializerSettings); + try + { + return JsonConvert.DeserializeObject(json, SerializerSettings); + } + catch (JsonReaderException ex) + { + throw DetailedJsonReaderException(ex, json); + } } public static object Deserialize(string json, Type type) { - return JsonConvert.DeserializeObject(json, type, SerializerSettings); + try + { + return JsonConvert.DeserializeObject(json, type, SerializerSettings); + } + catch (JsonReaderException ex) + { + throw DetailedJsonReaderException(ex, json); + } + } + + private static JsonReaderException DetailedJsonReaderException(JsonReaderException ex, string json) + { + var lineNumber = ex.LineNumber == 0 ? 0 : (ex.LineNumber - 1); + var linePosition = ex.LinePosition; + + var lines = json.Split('\n'); + if (lineNumber >= 0 && lineNumber < lines.Length && + linePosition >= 0 && linePosition < lines[lineNumber].Length) + { + var line = lines[lineNumber]; + var start = Math.Max(0, linePosition - 20); + var end = Math.Min(line.Length, linePosition + 20); + + var snippetBefore = line.Substring(start, linePosition - start); + var snippetAfter = line.Substring(linePosition, end - linePosition); + var message = ex.Message + " (Json snippet '" + snippetBefore + "<--error-->" + snippetAfter + "')"; + + // Not risking updating JSON.net from 9.x to 10.x just to get this as public ctor. + var ctor = typeof(JsonReaderException).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(Exception), typeof(string), typeof(int), typeof(int) }, null); + if (ctor != null) + { + return (JsonReaderException)ctor.Invoke(new object[] { message, ex, ex.Path, ex.LineNumber, linePosition }); + } + + // JSON.net 10.x ctor in case we update later. + ctor = typeof(JsonReaderException).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(string), typeof(int), typeof(int), typeof(Exception) }, null); + if (ctor != null) + { + return (JsonReaderException)ctor.Invoke(new object[] { message, ex.Path, ex.LineNumber, linePosition, ex }); + } + } + + return ex; } public static bool TryDeserialize(string json, out T result) @@ -77,10 +127,5 @@ namespace NzbDrone.Common.Serializer Serializer.Serialize(jsonTextWriter, model); jsonTextWriter.Flush(); } - - public static void Serialize(TModel model, Stream outputStream) - { - Serialize(model, new StreamWriter(outputStream)); - } } } diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileQualityItem.cs index 4b5369749..4fe8dec67 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileQualityItem.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileQualityItem.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Profiles.Qualities { public class QualityProfileQualityItem : IEmbeddedDocument { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int Id { get; set; } public string Name { get; set; } diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs index 7ed2dee13..016f6cf12 100644 --- a/src/NzbDrone.Core/Qualities/QualityModel.cs +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Qualities diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderRepository.cs b/src/NzbDrone.Core/ThingiProvider/ProviderRepository.cs index 626dafa19..ca347313b 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderRepository.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderRepository.cs @@ -11,8 +11,7 @@ using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.ThingiProvider { public class ProviderRepository : BasicRepository, IProviderRepository - where TProviderDefinition : ProviderDefinition, - new() + where TProviderDefinition : ProviderDefinition, new() { protected readonly JsonSerializerOptions _serializerSettings; diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index 9079e601a..199846894 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -5,7 +5,6 @@ - diff --git a/src/NzbDrone.Host/WebHost/WebHostController.cs b/src/NzbDrone.Host/WebHost/WebHostController.cs index d9013d07a..015e3cd2a 100644 --- a/src/NzbDrone.Host/WebHost/WebHostController.cs +++ b/src/NzbDrone.Host/WebHost/WebHostController.cs @@ -108,9 +108,9 @@ namespace NzbDrone.Host { services .AddSignalR() - .AddNewtonsoftJsonProtocol(options => + .AddJsonProtocol(options => { - options.PayloadSerializerSettings = Json.GetSerializerSettings(); + options.PayloadSerializerOptions = STJson.GetSerializerSettings(); }); }) .Configure(app => diff --git a/src/NzbDrone.SignalR/SignalRMessage.cs b/src/NzbDrone.SignalR/SignalRMessage.cs index 548d988a9..4b468477c 100644 --- a/src/NzbDrone.SignalR/SignalRMessage.cs +++ b/src/NzbDrone.SignalR/SignalRMessage.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using NzbDrone.Core.Datastore.Events; namespace NzbDrone.SignalR @@ -8,7 +7,7 @@ namespace NzbDrone.SignalR public object Body { get; set; } public string Name { get; set; } - [JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] public ModelAction Action { get; set; } } } diff --git a/src/Sonarr.Api.V3/Commands/CommandResource.cs b/src/Sonarr.Api.V3/Commands/CommandResource.cs index aa919a620..7b2461ebf 100644 --- a/src/Sonarr.Api.V3/Commands/CommandResource.cs +++ b/src/Sonarr.Api.V3/Commands/CommandResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Messaging.Commands; diff --git a/src/Sonarr.Api.V3/CustomFilters/CustomFilterResource.cs b/src/Sonarr.Api.V3/CustomFilters/CustomFilterResource.cs index aa71a34d8..a521bce84 100644 --- a/src/Sonarr.Api.V3/CustomFilters/CustomFilterResource.cs +++ b/src/Sonarr.Api.V3/CustomFilters/CustomFilterResource.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Dynamic; using System.Linq; using NzbDrone.Common.Serializer; using NzbDrone.Core.CustomFilters; @@ -10,7 +11,7 @@ namespace Sonarr.Api.V3.CustomFilters { public string Type { get; set; } public string Label { get; set; } - public List Filters { get; set; } + public List Filters { get; set; } } public static class CustomFilterResourceMapper @@ -23,12 +24,12 @@ namespace Sonarr.Api.V3.CustomFilters } return new CustomFilterResource - { - Id = model.Id, - Type = model.Type, - Label = model.Label, - Filters = Json.Deserialize>(model.Filters) - }; + { + Id = model.Id, + Type = model.Type, + Label = model.Label, + Filters = STJson.Deserialize>(model.Filters) + }; } public static CustomFilter ToModel(this CustomFilterResource resource) @@ -39,12 +40,12 @@ namespace Sonarr.Api.V3.CustomFilters } return new CustomFilter - { - Id = resource.Id, - Type = resource.Type, - Label = resource.Label, - Filters = Json.ToJson(resource.Filters) - }; + { + Id = resource.Id, + Type = resource.Type, + Label = resource.Label, + Filters = STJson.ToJson(resource.Filters) + }; } public static List ToResource(this IEnumerable filters) diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs index ecd276b5d..8563cad9c 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; @@ -68,14 +68,10 @@ namespace Sonarr.Api.V3.Indexers // Sent when queuing an unknown release - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - -// [JsonIgnore] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int? SeriesId { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - -// [JsonIgnore] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int? EpisodeId { get; set; } } diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index 8dfc9198e..f4e7fa651 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -8,7 +8,6 @@ - diff --git a/src/Sonarr.Api.V3/Update/UpdateResource.cs b/src/Sonarr.Api.V3/Update/UpdateResource.cs index 40c2dd426..629df409e 100644 --- a/src/Sonarr.Api.V3/Update/UpdateResource.cs +++ b/src/Sonarr.Api.V3/Update/UpdateResource.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; using NzbDrone.Core.Update; using Sonarr.Http.REST; @@ -9,7 +8,6 @@ namespace Sonarr.Api.V3.Update { public class UpdateResource : RestResource { - [JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))] public Version Version { get; set; } public string Branch { get; set; } diff --git a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs index 23e7be775..4263cd32a 100644 --- a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs @@ -2,10 +2,11 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Newtonsoft.Json.Linq; +using System.Text.Json; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; namespace Sonarr.Http.ClientSchema @@ -216,9 +217,9 @@ namespace Sonarr.Http.ClientSchema { return Enumerable.Empty(); } - else if (fieldValue.GetType() == typeof(JArray)) + else if (fieldValue is JsonElement e && e.ValueKind == JsonValueKind.Array) { - return ((JArray)fieldValue).Select(s => s.Value()); + return e.EnumerateArray().Select(s => s.GetInt32()); } else { @@ -234,9 +235,9 @@ namespace Sonarr.Http.ClientSchema { return Enumerable.Empty(); } - else if (fieldValue.GetType() == typeof(JArray)) + else if (fieldValue is JsonElement e && e.ValueKind == JsonValueKind.Array) { - return ((JArray)fieldValue).Select(s => s.Value()); + return e.EnumerateArray().Select(s => s.GetString()); } else { @@ -246,7 +247,18 @@ namespace Sonarr.Http.ClientSchema } else { - return fieldValue => fieldValue; + return fieldValue => + { + var element = fieldValue as JsonElement?; + + if (element == null || !element.HasValue) + { + return null; + } + + var json = element.Value.GetRawText(); + return STJson.Deserialize(json, propertyType); + }; } } diff --git a/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs b/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs index e7a42702b..66604b2a2 100644 --- a/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs +++ b/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Text.Json; using Nancy; using Nancy.Responses.Negotiation; using NzbDrone.Common.Serializer; @@ -8,6 +9,13 @@ namespace Sonarr.Http.Extensions { public class NancyJsonSerializer : ISerializer { + protected readonly JsonSerializerOptions _serializerSettings; + + public NancyJsonSerializer() + { + _serializerSettings = STJson.GetSerializerSettings(); + } + public bool CanSerialize(MediaRange contentType) { return contentType == "application/json"; @@ -15,7 +23,7 @@ namespace Sonarr.Http.Extensions public void Serialize(MediaRange contentType, TModel model, Stream outputStream) { - Json.Serialize(model, outputStream); + STJson.Serialize(model, outputStream, _serializerSettings); } public IEnumerable Extensions { get; private set; } diff --git a/src/Sonarr.Http/Extensions/ReqResExtensions.cs b/src/Sonarr.Http/Extensions/ReqResExtensions.cs index 1e854ab94..1cce0e46d 100644 --- a/src/Sonarr.Http/Extensions/ReqResExtensions.cs +++ b/src/Sonarr.Http/Extensions/ReqResExtensions.cs @@ -28,10 +28,8 @@ namespace Sonarr.Http.Extensions public static object FromJson(this Stream body, Type type) { - var reader = new StreamReader(body, true); body.Position = 0; - var value = reader.ReadToEnd(); - return Json.Deserialize(value, type); + return STJson.Deserialize(body, type); } public static JsonResponse AsResponse(this TModel model, NancyContext context, HttpStatusCode statusCode = HttpStatusCode.OK) diff --git a/src/Sonarr.Http/REST/RestModule.cs b/src/Sonarr.Http/REST/RestModule.cs index 60209a2ba..94363df25 100644 --- a/src/Sonarr.Http/REST/RestModule.cs +++ b/src/Sonarr.Http/REST/RestModule.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using FluentValidation; using FluentValidation.Results; using Nancy; using Nancy.Responses.Negotiation; -using Newtonsoft.Json; using NzbDrone.Core.Datastore; using Sonarr.Http.Extensions; @@ -233,7 +233,7 @@ namespace Sonarr.Http.REST { resource = Request.Body.FromJson(); } - catch (JsonReaderException e) + catch (JsonException e) { throw new BadRequestException($"Invalid request body. {e.Message}"); } @@ -330,7 +330,6 @@ namespace Sonarr.Http.REST } // v3 uses filters in key=value format - foreach (var key in Request.Query) { if (_excludedKeys.Contains(key)) @@ -339,10 +338,10 @@ namespace Sonarr.Http.REST } pagingResource.Filters.Add(new PagingResourceFilter - { - Key = key, - Value = Request.Query[key] - }); + { + Key = key, + Value = Request.Query[key] + }); } return pagingResource; diff --git a/src/Sonarr.Http/REST/RestResource.cs b/src/Sonarr.Http/REST/RestResource.cs index 9eada5853..2472bbea5 100644 --- a/src/Sonarr.Http/REST/RestResource.cs +++ b/src/Sonarr.Http/REST/RestResource.cs @@ -1,11 +1,11 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Sonarr.Http.REST { public abstract class RestResource { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public int Id { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public virtual int Id { get; set; } [JsonIgnore] public virtual string ResourceName => GetType().Name.ToLowerInvariant().Replace("resource", ""); diff --git a/src/Sonarr.Http/Sonarr.Http.csproj b/src/Sonarr.Http/Sonarr.Http.csproj index a8ef43162..ffa9568c3 100644 --- a/src/Sonarr.Http/Sonarr.Http.csproj +++ b/src/Sonarr.Http/Sonarr.Http.csproj @@ -7,7 +7,6 @@ -