diff --git a/src/NzbDrone.Common/Serializer/Json.cs b/src/NzbDrone.Common/Serializer/Json.cs index 31e0d3f0b..5f8cbfd8d 100644 --- a/src/NzbDrone.Common/Serializer/Json.cs +++ b/src/NzbDrone.Common/Serializer/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; @@ -14,13 +15,13 @@ namespace NzbDrone.Common.Serializer static Json() { SerializerSetting = new JsonSerializerSettings - { - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented, - DefaultValueHandling = DefaultValueHandling.Include, - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; + { + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented, + DefaultValueHandling = DefaultValueHandling.Include, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true }); @@ -34,12 +35,61 @@ namespace NzbDrone.Common.Serializer public static T Deserialize(string json) where T : new() { - return JsonConvert.DeserializeObject(json, SerializerSetting); + try + { + return JsonConvert.DeserializeObject(json, SerializerSetting); + } + catch (JsonReaderException ex) + { + throw DetailedJsonReaderException(ex, json); + } } public static object Deserialize(string json, Type type) { - return JsonConvert.DeserializeObject(json, type, SerializerSetting); + try + { + return JsonConvert.DeserializeObject(json, type, SerializerSetting); + } + 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) where T : new() @@ -78,4 +128,4 @@ namespace NzbDrone.Common.Serializer Serialize(model, new StreamWriter(outputStream)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Libraries.Test/JsonTests/JsonFixture.cs b/src/NzbDrone.Libraries.Test/JsonTests/JsonFixture.cs index fc4d58c6d..064028e9d 100644 --- a/src/NzbDrone.Libraries.Test/JsonTests/JsonFixture.cs +++ b/src/NzbDrone.Libraries.Test/JsonTests/JsonFixture.cs @@ -1,3 +1,4 @@ +using System; using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Serializer; @@ -24,5 +25,31 @@ namespace NzbDrone.Libraries.Test.JsonTests result.ShouldBeEquivalentTo(quality, o => o.IncludingAllRuntimeProperties()); } + + [Test] + public void should_log_start_snippet_on_failure() + { + try + { + Json.Deserialize("asdfl kasjd fsdfs derers"); + } + catch (Exception ex) + { + ex.Message.Should().Contain("snippet '<--error-->asdfl kasjd fsdfs de'"); + } + } + + [Test] + public void should_log_line_snippet_on_failure() + { + try + { + Json.Deserialize("{ \"a\": \r\n\"b\",\r\n \"b\": \"c\", asdfl kasjd fsdfs derers vsdfsdf"); + } + catch (Exception ex) + { + ex.Message.Should().Contain("snippet ' \"b\": \"c\", asdfl <--error-->kasjd fsdfs derers v'"); + } + } } }