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 converter append additional property during serialization #56297

Closed
dazinator opened this issue Jul 26, 2021 · 7 comments
Closed
Labels
area-System.Text.Json question Answer questions and provide assistance, not an issue with source code or documentation.

Comments

@dazinator
Copy link

dazinator commented Jul 26, 2021

I'm implementing a converter for system.text.json
When writing / serializing the object I want to add an additional property that isn't part of its type definition.

Here is the method signature I have to implement for the converter:


 public override void Write(
            Utf8JsonWriter writer,
            TriggerConfig value,
            JsonSerializerOptions options)
        {


       }

TriggerConfig is an abstract class. I'm doing polymorphic serialisation / deserilisation here. I've followed the docs for the deserialisation side, by taking a clone of the reader (which is a struct) reading ahead to validate the descriminator property, then if it's allowed, using the original reader to read the full object.

This is because I want a flat json representation of the object:

{
"MyDescriminator":"foo",
"PropA":"bar",
"PropB":"bat
}

However on the serialization side, when writing the object, I need to write the object, but first include this descrimator as an additional property.

The problem is, when implementing this Write method on the converter, I can't write the object, and then tag on an additional property because JsonSerializer.Serialize(writer, value, value.GetType()); writes the start and end object tokens giving me no opportunity to add a shadow property (a property that isn't part of the type definition).

 JsonSerializer.Serialize(writer, derivedA, derivedType); // how to I now write the additional shadow property as part of this object? 

I can workaround this problem by changing the structure of the json so that descrimator property sits outside the object:

{
"MyDescriminator":"foo",
"Object": {
  "PropA":"bar",
  "PropB":"bat
}
}

But that's un-necessary wrapper purely added to get around this underlying issue!

@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Text.Json untriaged New issue has not been triaged by the area owner labels Jul 26, 2021
@ghost
Copy link

ghost commented Jul 26, 2021

Tagging subscribers to this area: @eiriktsarpalis, @layomia
See info in area-owners.md if you want to be subscribed.

Issue Details

I'm implementing a converter.
When writing the object I want to add an additional property.

Here is the method signature I have to implement for the converter:


 public override void Write(
            Utf8JsonWriter writer,
            TriggerConfig value,
            JsonSerializerOptions options)
        {


       }

TriggerConfig is an abstract class. I'm doing polymorphic serialisation / deserilisation here. I've followed the docs for the deserialisation side, by taking a clone of the reader (which is a struct) reading ahead to validate the descriminator property, then if it's allowed, using the original reader to read the full object.

This is because I want a flat json representation of the object:

{
"MyDescrimator":"foo",
"PropA":"bar",
"PropB":"bat
}

However on the serialization side, when writing the object, I need to write the object, but first include this descrimator as an additional property.

The problem is, when implementing this Write method on the converter, I can't write the object, and then tag on an additional property because JsonSerializer.Serialize(writer, value, value.GetType()); writes the start and end object tokens giving me no opportunity to add a shadow property (a property that isn't part of the type definition).

 JsonSerializer.Serialize(writer, derivedA, derivedType); // how to I now write the additional shadow property as part of this object? 

I can workaround this problem by changing the structure of the json so that descrimator property sits outside the object:

{
"MyDescrimator":"foo",
"Object": {
  "PropA":"bar",
  "PropB":"bat
}
}

But that's un-necessary wrapper purely added to get around this underlying issue!

Author: dazinator
Assignees: -
Labels:

area-System.Text.Json, untriaged

Milestone: -

@eiriktsarpalis
Copy link
Member

Have you considered exposing a MyDescriminator property in the TriggerConfig base class? That would ensure that the default converter for each subtype would emit that information in one object. The other alternative I can think of is writing custom converters for each subtype that hardcodes the appropriate type discriminator (although that might require a lot of boilerplate).

Other than that, I would expect this issue to be addressed when #30083 is implemented.

@dazinator
Copy link
Author

dazinator commented Jul 26, 2021

@eiriktsarpalis

Have you considered exposing a MyDescriminator property in the TriggerConfig base class?

Yes, I'd prefer not to do this for multiple reasons which I'd be happy to go into.

The other alternative I can think of is writing custom converters for each subtype that hardcodes the appropriate type discriminator

Yeah thats not really going to be workable when you can have 10-15 derived types. I'd prefer a generic implementation so I can make proper use of generic types, and paramters so I don't have to write essentially the same class 10-15 times!

Other than that, I would expect this issue to be addressed when #30083 is implemented.

Ok will keep my eyes peeled. Another alternative i've considered is:

  • Serializing the derived instance to json in memory, then parsing the json into a modifiable structure like a JsonDocument then adding the property, then writing that. However I got stuck after parsing the json into a JsonDocument because JsonDocument does not let me make modifications. I considered also not parsing the serialized json but treating it as a string, modifying it in memory to include the additional json property, and then trying to write that json as is, but that method also failed because the Utf8JsonWriter doesn't just let me write some arbitrary json, I have to write individual properties, or tokens which means I need a structured view of the JSON.

@dazinator
Copy link
Author

Thinking on it some more, I think i've found this workaround:

            writer.WriteStartObject();
            writer.WriteString(TypeDescriminatorPropertyName, name);

            var json = JsonSerializer.Serialize(value, value.GetType());
            var document = JsonDocument.Parse(json);

            var root = document.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
            {
                throw new NotSupportedException();
            }            

            foreach (var property in root.EnumerateObject())
            {
                property.WriteTo(writer);
            }

            writer.WriteEndObject();
            writer.Flush();

@eiriktsarpalis
Copy link
Member

That might work, assuming you can tolerate the additional cycles used in the intermediate serializations and deserializations.

@eiriktsarpalis eiriktsarpalis added customer assistance and removed untriaged New issue has not been triaged by the area owner labels Jul 26, 2021
@dazinator
Copy link
Author

Side note related to same problem in different area of the stack: Even if this solves the problem for System.Text.Json, I have the same problem for Microsoft.Extensions.Options when binding an options class with a polymorphic list, to an IConfiguration section. In this case the a json file is first read by ConfigurationBuilder.AddJsonFile("appsettings.json") and ends up as essentially key value pairs. The descriminator property is now a key value pair in the IConfiguration. When materialising IOptions from this IConfiguration, there is now no JSON converter involved, so if I want to handle polymorphic lists I have to implement a seperate solution here, and it seems like there isn't a way to take control of binding yet.

@eiriktsarpalis
Copy link
Member

I've forked your last question into a separate issue. Assuming your initial question has been answered, I'm going to close this as a duplicate of #30083.

@ghost ghost locked as resolved and limited conversation to collaborators Aug 25, 2021
@eiriktsarpalis eiriktsarpalis added question Answer questions and provide assistance, not an issue with source code or documentation. and removed customer assistance labels Oct 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
None yet
Development

No branches or pull requests

2 participants