-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Serialization of properties from inherited interfaces vs abstract base classes #41749
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
System.Text.Json uses the declared type ( Also see a corresponding issue for polymorphic deserialization - #30083. |
It is not about the runtime type. Why does it not discover the base properties from the base interface. |
Discovering the base properties from base interfaces can be tracked individually from (but designed/implemented alongside) the polymorphic (de)serialization features. We should probably do it by default (potentially breaking as more properties may be (de)serialized), but provide an option to turn off this behavior. This is not a regression from 3.1, so I'll set the milestone as 6.0. |
From @ramondeklein in #47753:
|
Anybody know which preview this feature will start to light up in? Just curious. |
When I think of it. This is just how it should be. Any type that deserializes to iderived should be sufficient. (Inherited interfaces are just runtime implementations) If you have polymorphic serialization it would be the runtime type. For which I created a polymorphic converter. Which is pretty straight forward. Please check the converter here #29937 (comment) |
Inherited interfaces are in some ways similar to implementation inheritance: multiple inheritance is not permitted and members of the base interface are considered part of the signature for the derived interface. The hierarchy public interface IBase
{
string BaseProperty { get; set; }
}
public interface IDerived : IBase
{
string DerivedProperty { get; set; }
} should result in the same JSON contract as: public class Base
{
public string BaseProperty { get; set; }
}
public class Derived : Base
{
public string DerivedProperty { get; set; }
}
public interface IBase
{
string BaseProperty { get; set; }
}
public class Derived : IBase
{
public string DerivedProperty { get; set; }
string IBase.BaseProperty { get; set; } // not serialized
} |
Maybe explicit interface can just be named explicitly “Ibase.baseproperty” |
We should check what the behaviour of Newtonsoft.Json is in this case before making the change. |
We didn't get to this in .NET 6 and any change here is risky given where we are in the release cycle. We can consider for .NET 7. |
Well that's disappointing. Abstract classes are the only alternative for now? |
Sharing #73121 (comment) since it turns out that addressing this issue is less trivial that what we might have originally thought. |
I'm bumping priority here - we should at least consider 8.0 fix here |
It's pretty unfortunate that after several years of I created a public class JsonSerializeImplementation<TInterface> : JsonConverter<TInterface>
{
public override TInterface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> throw new NotSupportedException();
public override void Write(Utf8JsonWriter writer, TInterface value, JsonSerializerOptions options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
} This can be added to an interface by annotating the type with [AttributeUsage(AttributeTargets.Interface)]
public class JsonSerializeImplementationAttribute : Attribute
{
} Of course, just adding an attribute to a type doesn't make a real difference, so I created a public class JsonSerializeImplementationConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.GetCustomAttribute<JsonSerializeImplementationAttribute>() != null;
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
=> (JsonConverter?)Activator.CreateInstance(
typeof(JsonSerializeImplementation<>).MakeGenericType(typeToConvert),
BindingFlags.Instance | BindingFlags.Public,
null,
Array.Empty<object>(),
null);
} All you need to do is to register this factory using There are some severe drawbacks to this solution:
|
There exist simple way to workaround this scenario currently (more accurate one is further below): using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
Data data = new Data
{
Derived = new Derived
{
BaseProperty = "B",
DerivedProperty = "D"
}
};
Console.WriteLine(JsonSerializer.Serialize(data));
// {"Derived":{"$type":"Derived","BaseProperty":"B","DerivedProperty":"D"}}
public interface IBase
{
string BaseProperty { get; set; }
}
[JsonPolymorphic]
[JsonDerivedType(typeof(Derived), "Derived")]
public interface IDerived : IBase
{
string DerivedProperty { get; set; }
}
public class Derived : IDerived
{
public string BaseProperty { get; set; }
public string DerivedProperty { get; set; }
}
public class Data
{
public IDerived Derived { get; set; }
} and slightly more elaborate one:
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
Data data = new Data
{
Derived = new Derived
{
BaseProperty = "B",
DerivedProperty = "D"
}
};
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers =
{
AddAllInterfacesModifier
}
}
};
Console.WriteLine(JsonSerializer.Serialize(data, options));
// {"Derived":{"DerivedProperty":"D","BaseProperty":"B"}}
static void AddAllInterfacesModifier(JsonTypeInfo typeInfo)
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
if (typeInfo.Type.IsInterface)
{
Type[] interfaces = typeInfo.Type.GetInterfaces();
if (interfaces.Length == 0)
return;
Dictionary<string, JsonPropertyInfo> properties = new();
foreach (var prop in typeInfo.Properties)
{
properties.Add(prop.Name, prop);
}
foreach (var inter in typeInfo.Type.GetInterfaces())
{
var interTypeInfo = typeInfo.Options.GetTypeInfo(inter);
foreach (var prop in interTypeInfo.Properties)
{
JsonPropertyInfo propUnderNewTypeInfo = CopyProperty(prop, typeInfo);
if (properties.TryAdd(prop.Name, propUnderNewTypeInfo))
{
// we don't want to add duplicate names here
typeInfo.Properties.Add(propUnderNewTypeInfo);
}
}
}
}
}
static JsonPropertyInfo CopyProperty(JsonPropertyInfo property, JsonTypeInfo newTypeInfo)
{
JsonPropertyInfo copy = newTypeInfo.CreateJsonPropertyInfo(property.PropertyType, property.Name);
copy.AttributeProvider = property.AttributeProvider;
copy.Order = property.Order;
copy.Set = property.Set;
copy.Get = property.Get;
copy.CustomConverter = property.CustomConverter;
copy.IsExtensionData = property.IsExtensionData;
copy.IsRequired = property.IsRequired;
copy.NumberHandling = property.NumberHandling;
copy.ShouldSerialize = property.ShouldSerialize;
return copy;
}
public interface IBase
{
string BaseProperty { get; set; }
}
public interface IDerived : IBase
{
string DerivedProperty { get; set; }
}
public class Derived : IDerived
{
public string BaseProperty { get; set; }
public string DerivedProperty { get; set; }
}
public class Data
{
public IDerived Derived { get; set; }
} |
Let's keep this in 8.0.0 since it's being requested by a partner team. |
I guess .Net offers pretty much solutions to serialize interfaces the way you expect them to. I don't think the framework needs to provide default converters for it. How about explicit property implementations? Keep them? Thats something the class decides, not the interface. So you can't tell for sure. How about overlapping properties? Just to name some possible issues. JTS7 offers quite perfect solutions to customize (interface) serialization your way. By the way: a big thank you to the team for this nice lib. |
Note that this issue concerns contracts of interface types, not classes. Using the example given in the OP, the change would impact the serialization contract of values serialized as |
I would expect that inherited properties of interfaces would be serialized similar to (abstract) base classes.
Given:
Where the json string is missing the BaseProperty:
{"Derived":{"DerivedProperty":"D"}}
replacing the interface with an abstract class results in:
{"Derived":{"DerivedProperty":"D","BaseProperty":"B"}}
The text was updated successfully, but these errors were encountered: