Reputation: 16358
I have the following situation. I have simplified the problem into the following example, although my real situation is more complicated.
System.Text.Json does not serialise the object fully but Newtonsoft Json.NET does.
Suppose I have the following class structure.
public class A
{
public string AProperty { get; set; } = "A";
}
public class A<T> : A where T : class, new()
{
public T TObject { get; set; } = new T();
}
public class B
{
public string BProperty { get; set; } = "B";
}
public class B<T> : B where T : class, new()
{
public T TObject { get; set; } = new T();
}
public class C
{
public string CProperty { get; set; } = "C";
}
Here is a simple .NET Core program:
public class Program
{
private static void Main(string[] args)
{
var obj = new A<B> { TObject = new B<C>() };
var systemTextSerialized = JsonSerializer.Serialize(obj);
var newtonsoftSerialized = JsonConvert.SerializeObject(obj);
}
}
The serialised results are as follows:
{
"TObject": {
"BProperty": "B"
},
"AProperty": "A"
}
{
"TObject": {
"TObject": {
"CProperty": "C"
},
"BProperty": "B"
},
"AProperty": "A"
}
Due to the structure of my application, I don't know the generic parameter of B
. I only know that it is an A<B>
. The actual TObject
of B
is not known until runtime.
Why do these two serialisation methods differ? Is there a way to get System.Text.Json to serialise the object fully, or do I need to write a custom converter?
Upvotes: 7
Views: 17575
Reputation: 31
F# code assambles dynamically C# source and compiles and loads assambly on runtime, which extends F# abstract super class for OData server. System.text.json does not serialize C# fields, but does F# fields from superclass F#. Superclass in F# code:
//abstract for autogenerated code to fetch service signature and other common fields.
[<AbstractClass>]
type Signature() =
abstract member ServiceSignature: unit -> ODataService
[<JsonPropertyName("@id")>]
[<Key>]
member val public ``@id`` : int64 = 0 with get, set
//used for TEMPORAL table and for audit, visible only when defined in DTDL
[<JsonIgnore>]
member val ValidFrom : DateTime = DateTime.Now with get, set
[<JsonIgnore>]
member val ValidTo : DateTime = DateTime.MaxValue with get, set
[<JsonIgnore>]
member val ChangedBy : string = "system" with get, set
...
dynamically generated C# code:
...
[Serializable]
public class dtdlヽexampleヽdatatypesヽ1 : Signature
{
public override ODataService ServiceSignature()
{
return new ODataService( "dtdl/example/datatypes/1", ... }
// DigitalTwins fields
public bool aBoolean { get; set; }
...
[JsonPropertyName("@ValidFrom")]
public DateTime _ValidFrom { get => ValidFrom; }
to serialize using System.text.json use CAST TO OBJECT
let jsonStringA = JsonSerializer.Serialize(edmEntity :> obj, XJsonSerializerOptions())
Note: XJsonSerializerOptions() adds
let XJsonSerializerOptions() =
let options = JsonSerializerOptions()
// for ISO 8601 format
options.Converters.Add(TimeSpanConverter())
options.IncludeFields <- true
options
Upvotes: 0
Reputation: 116980
This is a documented limitation of System.Text.Json
. From the docs:
Serialize properties of derived classes
In versions prior to .NET 7, System.Text.Json doesn't support the serialization of polymorphic type hierarchies. For example, if a property is defined as an interface or an abstract class, only the properties defined on the interface or abstract class are serialized, even if the runtime type has additional properties. The exceptions to this behavior are explained in this section....
To serialize the properties of [a] derived type, use one of the following approaches:
Call an overload of Serialize that lets you specify the type at runtime...
Declare the object to be serialized as
object
.
In your case A<B>.TObject
is declared to be of type B
but is actually of type B<C>
in the instance you construct, so only the properties of the base class B
are getting serialized as per the documentation. So that's that. For further discussion see the closed issue System.Text.Json.JsonSerializer doesn't serialize properties from derived classes #31742.
There are several workarounds available, however. Firstly, you could construct obj
as its most possibly derived type A<B<C>>
:
var obj = new A<B<C>> { TObject = new B<C>() };
Now all properties of TObject
get serialized. Demo fiddle #1 here. But unfortunately you can't use this workaround since The actual TObject
of B
is not known until runtime.
Alternatively, if you only need to serialize your obj
, you could follow suggestion #2 from the docs and declare an object
-typed surrogate property, and serialize that:
public class A<T> : A where T : class, new()
{
[System.Text.Json.Serialization.JsonPropertyName("TObject")]
[Newtonsoft.Json.JsonIgnore]
public object SerializedTObject => TObject;
[System.Text.Json.Serialization.JsonIgnore]
public T TObject { get; set; } = new T();
}
Note that JsonSerializerOptions.IgnoreReadOnlyProperties
must not be set for read-only properties to be serialized.
Demo fiddle #2 here.
Finally, if you need polymorphic serialization and deserialization, you will need to write a custom JsonConverter
or (in .NET 7 and later) mark the base type with appropriate attribute annotations. To get started see
Upvotes: 12