diff --git a/src/NodaTime.Serialization.JsonNet/NodaConverterBase.cs b/src/NodaTime.Serialization.JsonNet/NodaConverterBase.cs index 2814489..bdfaedc 100644 --- a/src/NodaTime.Serialization.JsonNet/NodaConverterBase.cs +++ b/src/NodaTime.Serialization.JsonNet/NodaConverterBase.cs @@ -128,7 +128,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s /// to JSON. /// /// The writer to write JSON data to - /// The value to serializer + /// The value to serialize /// The serializer to use for nested serialization protected abstract void WriteJsonImpl(JsonWriter writer, T value, JsonSerializer serializer); } diff --git a/src/NodaTime.Serialization.JsonNet/NodaDateTimeZoneConverter.cs b/src/NodaTime.Serialization.JsonNet/NodaDateTimeZoneConverter.cs index ef07c0a..3aeab85 100644 --- a/src/NodaTime.Serialization.JsonNet/NodaDateTimeZoneConverter.cs +++ b/src/NodaTime.Serialization.JsonNet/NodaDateTimeZoneConverter.cs @@ -42,7 +42,7 @@ protected override DateTimeZone ReadJsonImpl(JsonReader reader, JsonSerializer s /// Writes the time zone ID to the writer. /// /// The writer to write JSON data to - /// The value to serializer + /// The value to serialize /// The serializer to use for nested serialization protected override void WriteJsonImpl(JsonWriter writer, DateTimeZone value, JsonSerializer serializer) { diff --git a/src/NodaTime.Serialization.JsonNet/NodaPatternConverter.cs b/src/NodaTime.Serialization.JsonNet/NodaPatternConverter.cs index 6826d55..dedad05 100644 --- a/src/NodaTime.Serialization.JsonNet/NodaPatternConverter.cs +++ b/src/NodaTime.Serialization.JsonNet/NodaPatternConverter.cs @@ -64,7 +64,7 @@ protected override T ReadJsonImpl(JsonReader reader, JsonSerializer serializer) /// Writes the formatted value to the writer. /// /// The writer to write JSON data to - /// The value to serializer + /// The value to serialize /// The serializer to use for nested serialization protected override void WriteJsonImpl(JsonWriter writer, T value, JsonSerializer serializer) { diff --git a/src/NodaTime.Serialization.SystemTextJson/NodaConverterBase.cs b/src/NodaTime.Serialization.SystemTextJson/NodaConverterBase.cs index 0ee4af8..d059181 100644 --- a/src/NodaTime.Serialization.SystemTextJson/NodaConverterBase.cs +++ b/src/NodaTime.Serialization.SystemTextJson/NodaConverterBase.cs @@ -64,6 +64,28 @@ public override T Read(ref Utf8JsonReader reader, Type objectType, JsonSerialize } } + /// + /// Converts the JSON stored in a reader into the relevant Noda Time type. + /// + /// The json reader to read data from. + /// The type to convert the JSON to. + /// A serializer options to use for any embedded deserialization. + /// The JSON was invalid for this converter. + /// The deserialized value. + public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + try + { + // Delegate to the concrete subclass. + return ReadJsonImpl(ref reader, options); + } + catch (Exception ex) + { + throw new JsonException($"Cannot convert value to {typeToConvert}", ex); + } + } + /// /// Implemented by concrete subclasses, this performs the final conversion from a non-null JSON value to /// a value of type T. @@ -86,13 +108,33 @@ public override T Read(ref Utf8JsonReader reader, Type objectType, JsonSerialize public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => WriteJsonImpl(writer, value, options); + /// + /// Writes the value as a string to a Utf8JsonWriter. + /// + /// The writer to write the JSON to. + /// The value to write. + /// The serializer options to use for any embedded serialization. + public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => + WriteJsonPropertyNameImpl(writer, value, options); + /// /// Implemented by concrete subclasses, this performs the final write operation for a non-null value of type T /// to JSON. /// /// The writer to write JSON data to - /// The value to serializer + /// The value to serialize /// The serializer options to use for nested serialization protected abstract void WriteJsonImpl(Utf8JsonWriter writer, T value, JsonSerializerOptions options); + + /// + /// Implemented by concrete subclasses, this performs the final write operation for a non-null value of type T + /// to JSON, writing the value as a property name. The default implementation throws + /// for compatibility purposes, but all concrete classes within this package override and implement the method fully. + /// + /// The writer to write JSON data to + /// The value to serialize + /// The serializer options to use for nested serialization + protected virtual void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => + throw new NotImplementedException(); } } diff --git a/src/NodaTime.Serialization.SystemTextJson/NodaDateIntervalConverter.cs b/src/NodaTime.Serialization.SystemTextJson/NodaDateIntervalConverter.cs index 627ac93..ffd1517 100644 --- a/src/NodaTime.Serialization.SystemTextJson/NodaDateIntervalConverter.cs +++ b/src/NodaTime.Serialization.SystemTextJson/NodaDateIntervalConverter.cs @@ -87,5 +87,15 @@ protected override void WriteJsonImpl(Utf8JsonWriter writer, DateInterval value, writer.WriteEndObject(); } + + /// + /// Unconditionally throws an exception, as a DateInterval cannot be serialized as a JSON property name. + /// + /// The writer to write JSON to + /// The date interval to serialize + /// The serializer options for embedded serialization. + /// Always thrown to indicate this is not an appropriate method to call on this type. + protected override void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, DateInterval value, JsonSerializerOptions options) => + throw new JsonException("Cannot serialize a DateInterval as a JSON property name using this converter"); } } diff --git a/src/NodaTime.Serialization.SystemTextJson/NodaDateTimeZoneConverter.cs b/src/NodaTime.Serialization.SystemTextJson/NodaDateTimeZoneConverter.cs index 8057d9b..307b72d 100644 --- a/src/NodaTime.Serialization.SystemTextJson/NodaDateTimeZoneConverter.cs +++ b/src/NodaTime.Serialization.SystemTextJson/NodaDateTimeZoneConverter.cs @@ -39,9 +39,18 @@ protected override DateTimeZone ReadJsonImpl(ref Utf8JsonReader reader, JsonSeri /// Writes the time zone ID to the writer. /// /// The writer to write JSON data to. - /// The value to serializer. + /// The value to serialize. /// The serialization options to use for nested serialization. protected override void WriteJsonImpl(Utf8JsonWriter writer, DateTimeZone value, JsonSerializerOptions options) => writer.WriteStringValue(value.Id); + + /// + /// Writes the time zone ID to the writer as a property name + /// + /// The writer to write JSON data to. + /// The value to serialize. + /// The serialization options to use for nested serialization. + protected override void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, DateTimeZone value, JsonSerializerOptions options) => + writer.WritePropertyName(value.Id); } } diff --git a/src/NodaTime.Serialization.SystemTextJson/NodaIntervalConverter.cs b/src/NodaTime.Serialization.SystemTextJson/NodaIntervalConverter.cs index 76ed48f..284f474 100644 --- a/src/NodaTime.Serialization.SystemTextJson/NodaIntervalConverter.cs +++ b/src/NodaTime.Serialization.SystemTextJson/NodaIntervalConverter.cs @@ -82,5 +82,15 @@ protected override void WriteJsonImpl(Utf8JsonWriter writer, Interval value, Jso } writer.WriteEndObject(); } + + /// + /// Unconditionally throws an exception, as an Interval cannot be serialized as a JSON property name. + /// + /// The writer to write JSON to + /// The date interval to serialize + /// The serializer options for embedded serialization. + /// Always thrown to indicate this is not an appropriate method to call on this type. + protected override void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, Interval value, JsonSerializerOptions options) => + throw new JsonException("Cannot serialize an Interval as a JSON property name using this converter"); } } diff --git a/src/NodaTime.Serialization.SystemTextJson/NodaIsoDateIntervalConverter.cs b/src/NodaTime.Serialization.SystemTextJson/NodaIsoDateIntervalConverter.cs index 78221df..91199fe 100644 --- a/src/NodaTime.Serialization.SystemTextJson/NodaIsoDateIntervalConverter.cs +++ b/src/NodaTime.Serialization.SystemTextJson/NodaIsoDateIntervalConverter.cs @@ -62,8 +62,15 @@ protected override DateInterval ReadJsonImpl(ref Utf8JsonReader reader, JsonSeri protected override void WriteJsonImpl(Utf8JsonWriter writer, DateInterval value, JsonSerializerOptions options) { var pattern = LocalDatePattern.Iso; - string text = pattern.Format(value.Start) + "/" + pattern.Format(value.End); + var text = $"{pattern.Format(value.Start)}/{pattern.Format(value.End)}"; writer.WriteStringValue(text); } + + protected override void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, DateInterval value, JsonSerializerOptions options) + { + var pattern = LocalDatePattern.Iso; + var text = $"{pattern.Format(value.Start)}/{pattern.Format(value.End)}"; + writer.WritePropertyName(text); + } } } diff --git a/src/NodaTime.Serialization.SystemTextJson/NodaIsoIntervalConverter.cs b/src/NodaTime.Serialization.SystemTextJson/NodaIsoIntervalConverter.cs index 7da1c49..ecb6d0e 100644 --- a/src/NodaTime.Serialization.SystemTextJson/NodaIsoIntervalConverter.cs +++ b/src/NodaTime.Serialization.SystemTextJson/NodaIsoIntervalConverter.cs @@ -52,8 +52,15 @@ protected override Interval ReadJsonImpl(ref Utf8JsonReader reader, JsonSerializ protected override void WriteJsonImpl(Utf8JsonWriter writer, Interval value, JsonSerializerOptions options) { var pattern = InstantPattern.ExtendedIso; - string text = (value.HasStart ? pattern.Format(value.Start) : "") + "/" + (value.HasEnd ? pattern.Format(value.End) : ""); + var text = $"{(value.HasStart ? pattern.Format(value.Start) : "")}/{(value.HasEnd ? pattern.Format(value.End) : "")}"; writer.WriteStringValue(text); } + + protected override void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, Interval value, JsonSerializerOptions options) + { + var pattern = InstantPattern.ExtendedIso; + var text = $"{(value.HasStart ? pattern.Format(value.Start) : "")}/{(value.HasEnd ? pattern.Format(value.End) : "")}"; + writer.WritePropertyName(text); + } } } diff --git a/src/NodaTime.Serialization.SystemTextJson/NodaPatternConverter.cs b/src/NodaTime.Serialization.SystemTextJson/NodaPatternConverter.cs index 2fb3113..458c324 100644 --- a/src/NodaTime.Serialization.SystemTextJson/NodaPatternConverter.cs +++ b/src/NodaTime.Serialization.SystemTextJson/NodaPatternConverter.cs @@ -2,9 +2,9 @@ // Use of this source code is governed by the Apache License 2.0, // as found in the LICENSE.txt file. +using NodaTime.Text; using System; using System.Text.Json; -using NodaTime.Text; namespace NodaTime.Serialization.SystemTextJson { @@ -59,12 +59,26 @@ protected override T ReadJsonImpl(ref Utf8JsonReader reader, JsonSerializerOptio /// Writes the formatted value to the writer. /// /// The writer to write JSON data to - /// The value to serializer + /// The value to serialize /// The serializer options to use for nested serialization protected override void WriteJsonImpl(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { validator?.Invoke(value); - writer.WriteStringValue(pattern.Format(value)); + var text = pattern.Format(value); + writer.WriteStringValue(text); + } + + /// + /// Writes the formatted value to the writer. + /// + /// The writer to write JSON data to. + /// The value to serialize. + /// The serialization options to use for nested serialization. + protected override void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + validator?.Invoke(value); + var text = pattern.Format(value); + writer.WritePropertyName(text); } } } \ No newline at end of file diff --git a/src/NodaTime.Serialization.Test/SystemTextJson/NodaConverterBaseTest.cs b/src/NodaTime.Serialization.Test/SystemTextJson/NodaConverterBaseTest.cs index a299b4f..9eb1d22 100644 --- a/src/NodaTime.Serialization.Test/SystemTextJson/NodaConverterBaseTest.cs +++ b/src/NodaTime.Serialization.Test/SystemTextJson/NodaConverterBaseTest.cs @@ -75,10 +75,11 @@ protected override int ReadJsonImpl(ref Utf8JsonReader reader, JsonSerializerOpt return int.Parse(reader.GetString()); } - protected override void WriteJsonImpl(Utf8JsonWriter writer, int value, JsonSerializerOptions options) - { + protected override void WriteJsonImpl(Utf8JsonWriter writer, int value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString()); - } + + protected override void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, int value, JsonSerializerOptions options) => + writer.WritePropertyName(value.ToString()); } private class TestStringConverter : NodaConverterBase @@ -88,10 +89,11 @@ protected override string ReadJsonImpl(ref Utf8JsonReader reader, JsonSerializer return reader.GetString(); } - protected override void WriteJsonImpl(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { + protected override void WriteJsonImpl(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value); - } + + protected override void WriteJsonPropertyNameImpl(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => + writer.WritePropertyName(value); } } } diff --git a/src/NodaTime.Serialization.Test/SystemTextJson/NodaConvertersTest.cs b/src/NodaTime.Serialization.Test/SystemTextJson/NodaConvertersTest.cs index a6b8df2..370b43a 100644 --- a/src/NodaTime.Serialization.Test/SystemTextJson/NodaConvertersTest.cs +++ b/src/NodaTime.Serialization.Test/SystemTextJson/NodaConvertersTest.cs @@ -3,6 +3,7 @@ // as found in the LICENSE.txt file. using System; +using System.Collections.Generic; using System.Text.Json; using NodaTime.Serialization.SystemTextJson; using NUnit.Framework; @@ -53,6 +54,39 @@ public void LocalDateConverter() AssertConversions(value, json, NodaConverters.LocalDateConverter); } + [Test] + public void LocalDateDictionaryKeySerialize() + { + const string expected = "{\"2012-12-21\":\"Mayan Calendar\",\"2012-12-22\":\"We Survived\"}"; + var actual = JsonSerializer.Serialize(new Dictionary + { + [new LocalDate(2012, 12, 21, CalendarSystem.Iso)] = "Mayan Calendar", + [new LocalDate(2012, 12, 22, CalendarSystem.Iso)] = "We Survived" + }, new JsonSerializerOptions + { + WriteIndented = false, + Converters = { NodaConverters.LocalDateConverter } + }); + Assert.AreEqual(expected, actual); + } + + [Test] + public void LocalDateDictionaryKeyDeserialize() + { + var expected = new Dictionary + { + [new LocalDate(2012, 12, 21, CalendarSystem.Iso)] = "Mayan Calendar", + [new LocalDate(2012, 12, 22, CalendarSystem.Iso)] = "We Survived" + }; + var actual = JsonSerializer.Deserialize>( + "{\"2012-12-21\":\"Mayan Calendar\",\"2012-12-22\":\"We Survived\"}", + new JsonSerializerOptions + { + Converters = { NodaConverters.LocalDateConverter } + }); + Assert.AreEqual(expected, actual); + } + [Test] public void LocalDateConverter_SerializeNonIso_Throws() { diff --git a/src/NodaTime.Serialization.Test/SystemTextJson/NodaDateIntervalConverterTest.cs b/src/NodaTime.Serialization.Test/SystemTextJson/NodaDateIntervalConverterTest.cs index a5974ba..a991993 100644 --- a/src/NodaTime.Serialization.Test/SystemTextJson/NodaDateIntervalConverterTest.cs +++ b/src/NodaTime.Serialization.Test/SystemTextJson/NodaDateIntervalConverterTest.cs @@ -4,6 +4,7 @@ using NodaTime.Serialization.SystemTextJson; using NUnit.Framework; +using System.Collections.Generic; using System.Text.Json; using static NodaTime.Serialization.Test.SystemText.TestHelper; @@ -135,7 +136,7 @@ public void Deserialize_CaseInsensitive() string json = "{\"Interval\":{\"Start\":\"2012-01-02\",\"End\":\"2013-06-07\"}}"; var testObjectPascalCase = JsonSerializer.Deserialize(json, optionsCaseInsensitive);; - var testObjectCamelCase = JsonSerializer.Deserialize(json, optionsCamelCaseCaseInsensitive); ; + var testObjectCamelCase = JsonSerializer.Deserialize(json, optionsCamelCaseCaseInsensitive); var intervalPascalCase = testObjectPascalCase.Interval; var intervalCamelCase = testObjectCamelCase.Interval; @@ -165,6 +166,16 @@ public void Deserialize_CaseInsensitive_CamelCase() Assert.AreEqual(expectedInterval, intervalCamelCase); } + [Test] + public void CannotUseDateIntervalAsPropertyName() + { + var obj = new Dictionary + { + { new DateInterval(new LocalDate(2012, 1, 2), new LocalDate(2013, 6, 7)), "Test" } + }; + Assert.Throws(() => JsonSerializer.Serialize(obj, options)); + } + public class TestObject { public DateInterval Interval { get; set; } diff --git a/src/NodaTime.Serialization.Test/SystemTextJson/NodaIntervalConverterTest.cs b/src/NodaTime.Serialization.Test/SystemTextJson/NodaIntervalConverterTest.cs index 4577a28..57cfa2b 100644 --- a/src/NodaTime.Serialization.Test/SystemTextJson/NodaIntervalConverterTest.cs +++ b/src/NodaTime.Serialization.Test/SystemTextJson/NodaIntervalConverterTest.cs @@ -2,6 +2,7 @@ // Use of this source code is governed by the Apache License 2.0, // as found in the LICENSE.txt file. +using System.Collections.Generic; using System.Text.Json; using NodaTime.Serialization.SystemTextJson; using NUnit.Framework; @@ -118,7 +119,7 @@ public void Deserialize_CaseSensitive() { string json = "{\"Interval\":{\"Start\":\"2012-01-02T03:04:05Z\",\"end\":\"2013-06-07T08:09:10Z\"}}"; - var testObject = JsonSerializer.Deserialize(json, options); ; + var testObject = JsonSerializer.Deserialize(json, options); Assert.True(testObject.Interval.HasStart); Assert.False(testObject.Interval.HasEnd); @@ -129,7 +130,7 @@ public void Deserialize_CaseSensitive_CamelCase() { string json = "{\"interval\":{\"Start\":\"2012-01-02T03:04:05Z\",\"end\":\"2013-06-07T08:09:10Z\"}}"; - var testObject = JsonSerializer.Deserialize(json, optionsCamelCase); ; + var testObject = JsonSerializer.Deserialize(json, optionsCamelCase); Assert.False(testObject.Interval.HasStart); Assert.True(testObject.Interval.HasEnd); @@ -140,8 +141,8 @@ public void Deserialize_CaseInsensitive() { string json = "{\"Interval\":{\"Start\":\"2012-01-02T03:04:05Z\",\"End\":\"2013-06-07T08:09:10Z\"}}"; - var testObjectPascalCase = JsonSerializer.Deserialize(json, optionsCaseInsensitive); ; - var testObjectCamelCase = JsonSerializer.Deserialize(json, optionsCamelCaseCaseInsensitive); ; + var testObjectPascalCase = JsonSerializer.Deserialize(json, optionsCaseInsensitive); + var testObjectCamelCase = JsonSerializer.Deserialize(json, optionsCamelCaseCaseInsensitive); var intervalPascalCase = testObjectPascalCase.Interval; var intervalCamelCase = testObjectCamelCase.Interval; @@ -171,6 +172,16 @@ public void Deserialize_CaseInsensitive_CamelCase() Assert.AreEqual(expectedInterval, intervalCamelCase); } + [Test] + public void CannotUseIntervalAsPropertyName() + { + var obj = new Dictionary + { + { new Interval(NodaConstants.UnixEpoch, NodaConstants.UnixEpoch), "Test" } + }; + Assert.Throws(() => JsonSerializer.Serialize(obj, options)); + } + public class TestObject { public Interval Interval { get; set; }