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

System.Text.Json embed object into custom jsonconverter #35240

Open
NinoFloris opened this issue Apr 21, 2020 · 10 comments
Open

System.Text.Json embed object into custom jsonconverter #35240

NinoFloris opened this issue Apr 21, 2020 · 10 comments
Labels
area-System.Text.Json wishlist Issue we would like to prioritize, but we can't commit we will get to it yet
Milestone

Comments

@NinoFloris
Copy link
Contributor

I'd like to know how to compose an outer generic type with an inner object in such a way the inner object is embedded (unwrapped/flattened) in the outer type during serialization.

Today JsonSerializer will always emit object start/end when called in a custom jsonconverter (as it would when calling it outside) There seems to be no mechanism to leave out the start/end delimiters. Together with the fact no metadata api is exposed by STJ means embedding is extremely difficult.

Example 'pseudocode'

public class Toggle<T> 
{
    private Toggle(bool enabled, T? data = null)
    {
        Enabled = enabled;
        Data = data;
    }

    public bool Enabled { get; }
    public T? Data { get; }

    public CreateDisabled() => new Toggle(false);
    public CreateEnabled(T data) => new Toggle(true, data);
}

public class Data 
{
    public bool Foo { get; set } 
    public int Bar { get; set; }
}

JsonSerializer.Serialize(Toggle<Data>.CreateEnabled(new Data { Foo = false, Bar = 1 });

I would want to have a way to create a converter such that the output of that Serialize call is embedding Data in Toggle like this.

{
    "enabled": true,
    "foo": false,
    "bar": 1
}
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Text.Json untriaged New issue has not been triaged by the area owner labels Apr 21, 2020
@ghost
Copy link

ghost commented Apr 21, 2020

Tagging subscribers to this area: @jozkee
Notify danmosemsft if you want to be subscribed.

@layomia
Copy link
Contributor

layomia commented Apr 21, 2020

Do you know of prior art in other serializers to achieve this flattening operation?

This is achievable with the converter model by handwriting each property as desired, either statically or with reflection:

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
					
public class Program
{
	public static void Main()
	{
		var options = new JsonSerializerOptions();
		options.Converters.Add(new ToggleConverter());

		string x = JsonSerializer.Serialize(Toggle<Data>.CreateEnabled(new Data { Foo = false, Bar = 1 }), options);
		Console.WriteLine(x);
	}
	
	public class Toggle<T> 
	{
		private Toggle(bool enabled, T data)
		{
			Enabled = enabled;
			Data = data;
		}

		public bool Enabled { get; }
		public T Data { get; }

		public static Toggle<T> CreateDisabled() => new Toggle<T>(false, (T)(object)null);
		public static Toggle<T> CreateEnabled(T data) => new Toggle<T>(true, data);
	}

	public class Data 
	{
		public bool Foo { get; set; } 
		public int Bar { get; set; }
	}

	public class ToggleConverter : JsonConverter<Toggle<Data>>
	{
		public override Toggle<Data> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
		{
			throw new NotImplementedException();
		}
		
		public override void Write(Utf8JsonWriter writer, Toggle<Data> value, JsonSerializerOptions options)
		{
		  	writer.WriteStartObject();

			// Written this way to show how complex objects can be written/composed with JsonSerializer.
		  	writer.WritePropertyName("enabled");
		  	JsonSerializer.Serialize(writer, value.Enabled);

		  	writer.WritePropertyName("foo");
		  	JsonSerializer.Serialize(writer, value.Data.Foo);

		  	writer.WritePropertyName("bar");
		  	JsonSerializer.Serialize(writer, value.Data.Bar);

		  	writer.WriteEndObject();
		}
	}
}

As you mention, exposing type/property metadata that the serializer relies on could make this easier in the future - #34456.

@layomia layomia removed the untriaged New issue has not been triaged by the area owner label Apr 21, 2020
@layomia layomia added this to the Future milestone Apr 21, 2020
@NinoFloris
Copy link
Contributor Author

NinoFloris commented Apr 21, 2020

This isn't easily possible for any T as was the point of me having Toggle<T> being a generic type.

  	// Written this way to show how complex objects can be written/composed with JsonSerializer.

I'd say calling it composition is pretty disingenuous , there's no composition of behavior here, just manual inlining.

@NinoFloris
Copy link
Contributor Author

NinoFloris commented Apr 21, 2020

https://www.logicbig.com/tutorials/misc/jackson/json-unwrapped.html

Jackson supports this

Probably quite some functional language serializers do as well, because this is key to great support for unions (which are often cross cutting generic types)

@layomia
Copy link
Contributor

layomia commented Apr 21, 2020

This isn't easily possible for any T as was the point of me having Toggle being a generic type.

That is just a concrete example. You can define a generic converter and use the factory pattern to handle various Ts:

https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#steps-to-follow-the-factory-pattern

@NinoFloris
Copy link
Contributor Author

NinoFloris commented Apr 21, 2020

I'm sorry, I'm a little bit frustrated by your response.

How can I embed a type I don't know of which properties exist? Maybe that T is a collection, or an object I'd like to have serialized exactly like JsonSerializer does, because maybe there's a custom converter registered for that T etcetera. It's not as simple as "just ask reflection for all public properties"

Think about it from the perspective of, Toggle lives in a library, T is up to the user.

@layomia
Copy link
Contributor

layomia commented Apr 21, 2020

I'm sorry, I'm a little bit frustrated by your response.

How can I embed a type I don't know of which properties exist? Maybe that T is a collection, or an object I'd like to have serialized exactly like JsonSerializer does, because maybe there's a custom converter registered for that T etcetera. It's not as simple as "just ask reflection for all public properties"

I agree that it is not easy to do the sort of composition you seek by reflecting over the properties of arbirtrary types, while honoring the options specified at the root call to the serializer (including using custom converters). I was just pointing out that it is in fact possible.

We are aware that "composition" is not easy to do with the current converter model, and plan to address this. However, this work is out of scope for 5.0.

@NinoFloris
Copy link
Contributor Author

Good to hear it's on your radar.

I do know about generic converters, they work well in general cases, mostly because T can be resolved via a call to the serializer, with the caveat this only works for the shape of a nested value. This is the main inflexibility I wanted to point out.

In any case thanks for taking the time.

@eiriktsarpalis
Copy link
Member

How can I embed a type I don't know of which properties exist? Maybe that T is a collection, or an object I'd like to have serialized exactly like JsonSerializer does, because maybe there's a custom converter registered for that T etcetera. It's not as simple as "just ask reflection for all public properties"

Correct me if I'm wrong, but my understanding of the unwrap feature is that it is constrained to T types that serialize as JSON objects. And given our converter model, it is likely this would never be supported for T types that define custom converters.

A possible solution might be to add JsonExtensionDataAttribute-like attribute that acts on properties with POCOs. It might be interesting to investigate how such a feature deals with naming conflicts (e.g. both Toggle<T> and Data containing a property with the same name).

@eiriktsarpalis eiriktsarpalis added the wishlist Issue we would like to prioritize, but we can't commit we will get to it yet label Oct 19, 2021
@eiriktsarpalis
Copy link
Member

Related to #55120.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Text.Json wishlist Issue we would like to prioritize, but we can't commit we will get to it yet
Projects
None yet
Development

No branches or pull requests

4 participants