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

JsonSerializer support for immutable classes and structs. #29895

Closed
johanneshoehn opened this issue Jun 15, 2019 · 15 comments · Fixed by #33444
Closed

JsonSerializer support for immutable classes and structs. #29895

johanneshoehn opened this issue Jun 15, 2019 · 15 comments · Fixed by #33444
Assignees
Labels
area-System.Text.Json json-functionality-doc Missing JSON specific functionality that needs documenting
Milestone

Comments

@johanneshoehn
Copy link

A common pattern is to make data objects immutable for many different reasons.
For example Point:

public class Point
{
    int X { get; }

    int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

It would be very helpful if JsonSerializer supported immutable classes/structs like that, especially since Newtonsoft Json.NET supports deserialization through the constructor.

However there are a few issues surrounding immutable types:

  • Constructor parameter names are usually camelCase while Properties are PascalCase. One way to solve this is by setting JsonSerializerOptions.PropertyNameCaseInsensitive in to true. Another way is to use the Deconstruct-pattern introduced in C# 7.0, instead of Properties for serialization.

  • There may be several constructors (and Deconstruct methods). So it's not always clear which to use. Newtonsoft Json.NET uses a JsonConstructorAttribute.htm, which has the disadvantage that the data objects need to know about serialization.

  • Constructor parameters, properties and Deconstruct method parameters might not match up, possibly leading to confusing situations where deserializing a previously serialized object does not work.

  • Immutable structs always still have a default parameterless constructor.

@johanneshoehn johanneshoehn changed the title JsonSerializer support for immutable classes and structs. JsonSerializer support for immutable classes and structs. Jun 15, 2019
@ahsonkhan
Copy link
Member

This is a known limitation of the System.Text.Json serializer for v1. We plan to support this in the future.

@steveharter
Copy link
Member

You can write a custom converter for this.

@Gnbrkm41
Copy link
Contributor

I totally was going to write an issue about this, glad I noticed this before that 🙃

I personally thought that writing a private setter on otherwise a readonly property purely because of JSON (de)serialization is kind of hacky. I know that I could write a custom converter (when it's implemented) or use a constructor (which I remember will be supported soon? I could be wrong) but... that's effort ☺️

@steveharter
Copy link
Member

For the 3.0 release, there is no planned additional support for calling a non-default constructor during deserialization. That would have to be done by a custom converter.

@Gnbrkm41
Copy link
Contributor

That's fine, as long as it's in the plan in some way... One step at a time 😁

@khellang
Copy link
Member

There's a fair bit of overlap with this issue and https://github.com/dotnet/corefx/issues/38569.

@Gnbrkm41
Copy link
Contributor

@khellang, it probably overlaps because this issue is dotnet/corefx#38569 😄 I guess you meant 41973?

@khellang
Copy link
Member

LOL, no, I meant https://github.com/dotnet/corefx/issues/40399 😂

@Mike-E-angelo
Copy link

3dzwv6
😅

@danstur
Copy link

danstur commented Nov 15, 2019

@ahsonkhan Is there work currently being done on this issue? If not, is this something where a pull request would be considered?

@git-net
Copy link

git-net commented Nov 23, 2019

seems would support Deserialize anonymous object?

@layomia
Copy link
Contributor

layomia commented Nov 25, 2019

From @endeffects in dotnet/corefx#40399:

Please provide support for custom constructors and allow to specify the constructor with an attribute.

@ahsonkhan
Copy link
Member

From @EdiWang in https://github.com/dotnet/corefx/issues/41102:

The following code throws exception.

static void Main(string[] args)
{
    var obj = new Response(true, "Hi") { ResponseCode = 100 };
    var json = System.Text.Json.JsonSerializer.Serialize(obj);

    // System.NotSupportedException: 'Deserialization of reference types without parameterless constructor is not supported.
    var response = System.Text.Json.JsonSerializer.Deserialize<Response>(json);

    Console.WriteLine(response.Message);
}

public class Response
{
    public bool IsSuccess { get; set; }

    public string Message { get; set; }

    public int ResponseCode { get; set; }

    public Response(bool isSuccess = false, string message = "")
    {
        IsSuccess = isSuccess;
        Message = message;
    }
}

Response class is considered not having a parameterless constructor, but actually it does use default values for parameters, that can be used just like a parameterless constructor, like this:

var test = new Response();

Could the new JSON API check if the constructor parameters have default values before consider it not paremeterless?

Related Issues:
#29490

PR:
dotnet/corefx#38061

@manne
Copy link
Contributor

manne commented Jan 15, 2020

I have started to implement a custom converter for immutable types here https://github.com/manne/obviously/tree/master/src/system.text.json.

The converter is able to read "basic" types. The type must have exactly one constructor with at least one parameter. The type has not be sealed.

Example

public sealed class ProgramSettings
{
    public ProgramSettings(Uri baseUrl, string adminName, string adminPassword)
    {
        BaseUrl = baseUrl;
        AdminName = adminName;
        AdminPassword = adminPassword;
    }

    public Uri BaseUrl { get; }
    public string AdminName { get; }
    public string AdminPassword { get; }
}

public void DeserializeSettings()
{
    const string settingsJson = @"{""BaseUrl"": ""https://example.com:8999"", ""AdminName"": ""admin"", ""AdminPassword"": ""topsecret""}";
    var options = new JsonSerializerOptions();
    options.Converters.Add(new ImmutableConverter());

    var programSettings = JsonSerializer.Deserialize<ProgramSettings>(settingsJson, options);
    var baseUrl = programSettings.BaseUrl; // https://example.com:8999
    var adminName = programSettings.AdminName; // admin
    var adminPassword = programSettings.AdminPassword; // topsecret
}

I have uploaded a nuget package Obviously.System.Text.Json, it is not yet indexed.

Feedback and PRs are always welcome 😉

@ahsonkhan
Copy link
Member

From @ahedreville in #1925:

I missed the Newtonsoft.Json the nice attribute JsonConstructor that is very handy when serializing classes that do not expose public constructor.. like the one below...

    public class Envelope<T>
    {
        public T Result { get; }
        public string ErrorMessage { get; }
        public DateTime TimeGenerated { get; }

        [JsonConstructor]                                             // This constructor is marked as the valid JsonConvert constructor for JsonConvert.DeserializeObject usage.
        protected internal Envelope(T result, string errorMessage)
        {
            Result = result;
            ErrorMessage = errorMessage;
            TimeGenerated = DateTime.UtcNow;
        }
    }

    public sealed class Envelope : Envelope<string>
    {
        private Envelope(string error)
        : base(null, error)
        {
        }

        public static Envelope Ok() => new Envelope(null);
        public static Envelope<T> Ok<T>(T result) => new Envelope<T>(result, default);

        public static Envelope Error(string error) => new Envelope(error);
        public static Envelope<T> Error<T>(T result) => new Envelope<T>(result, default);
        public static Envelope<T> Error<T>(T result, string error) => new Envelope<T>(result, error);
    }

ckuetbach pushed a commit to d-velop/dvelop-sdk-cs that referenced this issue Jun 2, 2020
System.Text,Json needs a parameterless default constructor: dotnet/runtime#29895 (comment)
jonclare added a commit to jonclare/domain-lib that referenced this issue Oct 22, 2020
…tion working.

Upgrading to .NET 5.0 should fix this as it supports working with classes without default constructors
dotnet/runtime#29895
daniel-smith pushed a commit to daniel-smith/domain-lib that referenced this issue Oct 27, 2020
* Start on persistence

* More work on deserializing events

* Add some custom converters as a hack to get serialization/deserialization working.
Upgrading to .NET 5.0 should fix this as it supports working with classes without default constructors
dotnet/runtime#29895

* Upgrade test assemblies to .NET 5.0 to take advantage of serialization fixes that allow JSON to be deserialized into classes without default constructors

* Add a custom attribute that the library uses to determine event names in the first instance and centralise the logic for mapping event names to types and vice versa

* Simplify repository by pulling all the logic to get event names into the serializer

* Add some tests for EventNameMapping

* Allow proper serialization of exceptions

* Test to ensure that building the event routes also wires up the event name mappings

* Simplify EventNameMap name and ensure it's consistent everywhere
@ghost ghost locked as resolved and limited conversation to collaborators Dec 12, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json json-functionality-doc Missing JSON specific functionality that needs documenting
Projects
None yet
Development

Successfully merging a pull request may close this issue.