Skip to content
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

[BUG] Json serialization behavior is not the same on framework as it is on core/5+ #71

Closed
KieranDevvs opened this issue Apr 19, 2024 · 13 comments
Assignees
Labels
documentation Improvements or additions to documentation enhancement New feature or request

Comments

@KieranDevvs
Copy link

KieranDevvs commented Apr 19, 2024

Describe the bug
Deserializing the structs on Framework from valid JSON results in a zero value.
Deserializing the structs on Core/NET 5+ results in the correct representation.

The JSON is serialized correctly but it would appear the fields aren't populated correctly when deserializing due to the fact that the properties have private setters. System.Text.Json gained the ability to deserialise properties with private setters in .NET 5.

Potential solutions:

  1. Add a [JsonConstructor] to the structs.
  2. Package a converter within the library and mark the struct with the converter.
public class TimeOnlyJsonConverter : JsonConverter<TimeOnly>
{
    public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return TimeOnly.Parse(reader.GetString());
    }
            
    public override void Write(Utf8JsonWriter writer, TimeOnly timeValue, JsonSerializerOptions options)
    {
        writer.WriteStringValue(timeValue.ToString());
    }
}
  1. Users have to write their own converters (undesirable as it does not maintain the behavior we have with the original usage of DateOnly and TimeOnly).

To Reproduce

#r "nuget: System.Text.Json, 8.0.3"
#r "nuget: Portable.System.DateTimeOnly, 8.0.0"

using System.Text.Json;

var a = new TimeOnly(1, 2);

var json = JsonSerializer.Serialize(a);

var b = JsonSerializer.Deserialize<TimeOnly>(json);

b.Dump();

.NET 8
image

.NET Framework 4.8
image

@KieranDevvs KieranDevvs added the bug Something isn't working label Apr 19, 2024
@OlegRa
Copy link
Owner

OlegRa commented Apr 20, 2024

Let me comment on your initial statement a little. You wrote:

The JSON is serialized correctly but it would appear the fields aren't populated correctly when deserializing due to the fact that the properties have private setters. System.Text.Json gained the ability to deserialize properties with private setters in .NET 5.

There is no "correct" serialization for this type (and for the DateOnly too). The System.Text.Json developers made some decisions on how to do it but it's not a part of JSON specification. You can also serialize these types as numbers if it's more appropriate for your use cases.

And BCL versions of DateOnly and TimeOnly types don't have serialization attributes. The BCL doesn't depend on the System.Text.Json library. But the System.Text.Json has a special code for handling these types on some platforms. Of course, the built-in TimeOnlyConverter will handle only the BCL-based version of the TimeOnly type.

I don't particularly appreciate adding dependencies like System.Text.Json into such a tiny library. Everyone who needs "portable" things should understand its limitations. But I agree that this limitation should be more "visible":

  • I can add a statement about this "known issue" to the README and encourage users to write converters
  • I can provide sample converters as a code in the README so everyone can copy-paste code and use it
  • I can create another NuGet package with 2 converters and write about this package in the README

And of course, you can create this new NuGet package with converters and I'll just add a link to it into README.

@OlegRa OlegRa added documentation Improvements or additions to documentation wontfix This will not be worked on and removed bug Something isn't working labels Apr 20, 2024
@OlegRa
Copy link
Owner

OlegRa commented Apr 20, 2024

@all-contributors please add @KieranDevvs for ideas

Copy link
Contributor

@OlegRa

I've put up a pull request to add @KieranDevvs! 🎉

@KieranDevvs
Copy link
Author

KieranDevvs commented Apr 20, 2024

@OlegRa Thanks for the feedback. I appreciate not wanting to add a serialization library dependency, and I think having a separate NuGet package for the converters would be great. I don't think this should be left to the user to implement as I see much more benefit and ease of use from standardising the converters to match the .NET core/5+ behaviour.

This would mean users would have to decorate their properties / fields with [JsonConverter(typeof(NameOfConverterType))] or add the converter directly to the JsonSerializerOptions. This is obviously only going to be the case if you're using System.Text.Json and any other serializer will need other workarounds (although I think Newtonsoft.Json doesn't have this problem, and I personally only care about the serializers that are available as part of the BCL). Subsequently this would give a place for users to contribute their own conversion types for other libraries.

@OlegRa OlegRa added enhancement New feature or request and removed wontfix This will not be worked on labels Apr 24, 2024
OlegRa added a commit that referenced this issue Apr 28, 2024
…t.Json` serialization/deserialization support.
@KieranDevvs
Copy link
Author

@OlegRa Oh I didn't expect you to create the project AND do the implementation, I was going to contribute but I wont argue about not having to do the work 🥳. Do you have an ETA on the NuGet package release? I can start swapping out my existing converters in place of yours 🙂.

@OlegRa
Copy link
Owner

OlegRa commented May 1, 2024

If you have an alternative implementation, you can create your own NuGet package. It's always better to have two OSS alternatives than one 🥇

My implementation is based on the System.Text.Json code base. The only interesting thing is making an attribute that will work for old and new frameworks without any ifdefs in the client code. My version fallbacks to the built-in converters in the case of .NET 6.0 and later.

I will publish this NuGet package later this evening (European time) after completing GitHub action for it.

@OlegRa
Copy link
Owner

OlegRa commented May 1, 2024

@KieranDevvs Finally it's published. Please, test and provide your feedback here or in a separate issue(s).

@osexpert
Copy link

osexpert commented May 3, 2024

I wonder...does it not suffice to use a type converter to\from string? I think both json.net and STJ would then just work without any extra work and no need for separate package?

@KieranDevvs
Copy link
Author

KieranDevvs commented May 3, 2024

I wonder...does it not suffice to use a type converter to\from string? I think both json.net and STJ would then just work without any extra work and no need for separate package?

I don't understand your question. Portable.System.DateTimeOnly.Json does provide a type converter. As mentioned in this issue, its not favorable to let the consumers each individually build their own. It leads to a worse experience as the type cant be used out of the box if you intend to serialize it. It also leads to non standardized serialization so one vendor might build their converter to serialize to .ToString() which would result in something like "YYYY-MM-DD hh:mm:ss" , and another provider might serialize it to a flattened object like so:

{
    "year": 2001,
    "month": 2,
    "day": 19,
    "hour": 17,
    ....
}

And now if these two applications want to interface each other, they need to: A) Share converter types or B) Standardize their converters.

TLDR; I think its better to offer a standard approach out of the box and if you decide to do your own thing, then fine, but you're to blame if it doesn't go well.

@osexpert
Copy link

osexpert commented May 3, 2024

I don't understand your question. Portable.System.DateTimeOnly.Json does provide a type converter

I mean, could it suffice to use a json independent type converter like this that could live in the main package (meaning, in the System.DateTimeOnly package itself):

 [TypeConverter(typeof(DateOnlyIsoTypeConverter))]
struct DateOnly
{
 ...
}

    public class DateOnlyIsoTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
            => sourceType == typeof(string);

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is string str && DateOnly.TryParseExact(str, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var ut))
            { 
                return ut;
            }

            return base.ConvertFrom(context, culture, value);
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string) && value is DateOnly don)
            {
                return don.ToString("o", CultureInfo.InvariantCulture);
            }

            return base.ConvertTo(context, culture, value, destinationType);
        }
    }

"As mentioned in this issue, its not favorable to let the consumers each individually build their own"
I have suggested no such thing

"TLDR; I think its better to offer a standard approach out of the box and if you decide to do your own thing, then fine, but you're to blame if it doesn't go well."
Yes, I have not suggested otherwise...

@OlegRa
Copy link
Owner

OlegRa commented May 3, 2024

@osexpert There are two problems with the TypeConverter approach:

  1. The original BCL types don't have this attribute and there is no default type converter(s) for them. Adding these attributes will not affect anyone (I hope), but it will make backported types not fully compatible with the BCL sources.
  2. Most importantly - the System.Text.Json library never uses the TypeConverter attribute for controlling the serialization/deserialization process (see this discussion).

The current approach is not ideal but it's better than nothing. Right now it has only one limitation - the source generator goes crazy and tries to use built-in converters for backported types even on unsupported platforms. For this scenario, we'll need a more complex workaround.

@osexpert
Copy link

osexpert commented May 3, 2024

  1. Most importantly - the System.Text.Json library never uses the TypeConverter attribute for controlling the serialization/deserialization process (see this discussion).

That is a problem for sure:-) I incorrectly remembered it did. Thanks.

@OlegRa
Copy link
Owner

OlegRa commented Jun 16, 2024

@KieranDevvs, I closed it as completed after this helper NuGet package's "official" release. If you have any issues/bugs or some ideas for better documentation, please open another issue(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants