Derived type's properties missing in JSON response from ASP.NET Core API

前端 未结 6 1865
无人共我
无人共我 2021-02-13 02:51

The JSON response from my ASP.NET Core 3.1 API controller is missing properties. This happens when a property uses a derived type; any properties defined in the derived type but

相关标签:
6条回答
  • 2021-02-13 03:01

    This is the expected result. You're upcasting when you do that, so what will be serialized is the upcasted object, not the actual derived type. If you need stuff from the derived type, then that has to be the type of the property. You may want to use generics for this reason. In other words:

    public class Result<TResultProperty>
        where TResultProperty : IResultProperty
    {
        public TResultProperty ResultProperty { get; set; }   // property uses an interface type
    }
    

    Then:

    return new Result<StringResultProperty> {
        ResultProperty = new StringResultProperty { Value = "Hi there!" }  
    };
    
    0 讨论(0)
  • 2021-02-13 03:02

    The documentation shows how to serialize as the derived class when calling the serializer directly. The same technique can also be used in a custom converter that we then can tag our classes with.

    First, create a custom converter

    public class AsRuntimeTypeConverter<T> : JsonConverter<T>
    {
        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return JsonSerializer.Deserialize<T>(ref reader, options);
        }
    
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        {
            JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(object), options);
        }
    }
    

    Then mark the relevant classes to be used with the new converter

    [JsonConverter(typeof(AsRuntimeTypeConverter<MyBaseClass>))]
    public class MyBaseClass
    {
       ...
    

    Alternately, the converter can be registered in startup.cs instead

    services
      .AddControllers(options =>
         .AddJsonOptions(options =>
                {
                    options.JsonSerializerOptions.Converters.Add(new AsRuntimeTypeConverter<MyBaseClass>());
                }));
    
    0 讨论(0)
  • 2021-02-13 03:08

    I ended up creating a custom JsonConverter (System.Text.Json.Serialization namespace) which forces JsonSerializer to serialize to the object's runtime type. See the Solution section below. It's lengthy but it works well and does not require me to sacrifice object oriented principles in my API's design.

    Some background: Microsoft has a System.Text.Json serialization guide with a section titled Serialize properties of derived classes with good information relevant to my question. In particular it explains why properties of derived types are not serialized:

    This behavior is intended to help prevent accidental exposure of data in a derived runtime-created type.

    If that is not a concern for you then the behavior can be overridden in the call to JsonSerializer.Serialize by either explicitly specifying the derived type or by specifying object, for example:

        // by specifying the derived type
        jsonString = JsonSerializer.Serialize(objToSerialize, objToSerialize.GetType(), serializeOptions);
    
        // or specifying 'object' works too
        jsonString = JsonSerializer.Serialize<object>(objToSerialize, serializeOptions);
    

    To accomplish this with ASP.NET Core you need to hook into the serialization process. I did this with a custom JsonConverter that calls JsonSerializer.Serialize one of the ways shown above. I also implemented support for deserialization which, while not explicitly asked for in the original question, is almost always needed anyway. (Oddly, supporting only serialization and not deserialization proved to be tricky anyway.)

    Solution

    I created a base class, DerivedTypeJsonConverter, which contains all of the serialization & deserialization logic. For each of your base types, you would create a corresponding converter class for it that derives from DerivedTypeJsonConverter. This is explained in the numbered directions below.

    This solution follows the "type name handling" convention from Json.NET which introduces support for polymorphism to JSON. It works by including an additional $type property in the derived type's JSON (ex: "$type":"StringResultProperty") that tells the converter what the object's true type is. (One difference: in Json.NET, $type's value is a fully qualified type + assembly name, whereas my $type is a custom string which helps future-proof against namespace/assembly/class name changes.) API callers are expected to include $type properties in their JSON requests for derived types. The serialization logic solves my original problem by ensuring that all of the object's public properties are serialized, and for consistency the $type property is also serialized.

    Directions:

    1) Copy the DerivedTypeJsonConverter class below into your project.

        using System;
        using System.Collections.Generic;
        using System.Dynamic;
        using System.IO;
        using System.Linq;
        using System.Reflection;
        using System.Text;
        using System.Text.Json;
        using System.Text.Json.Serialization;
    
        public abstract class DerivedTypeJsonConverter<TBase> : JsonConverter<TBase>
        {
            protected abstract string TypeToName(Type type);
    
            protected abstract Type NameToType(string typeName);
    
    
            private const string TypePropertyName = "$type";
    
    
            public override bool CanConvert(Type objectType)
            {
                return typeof(TBase) == objectType;
            }
    
    
            public override TBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                // get the $type value by parsing the JSON string into a JsonDocument
                JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader);
                jsonDocument.RootElement.TryGetProperty(TypePropertyName, out JsonElement typeNameElement);
                string typeName = (typeNameElement.ValueKind == JsonValueKind.String) ? typeNameElement.GetString() : null;
                if (string.IsNullOrWhiteSpace(typeName)) throw new InvalidOperationException($"Missing or invalid value for {TypePropertyName} (base type {typeof(TBase).FullName}).");
    
                // get the JSON text that was read by the JsonDocument
                string json;
                using (var stream = new MemoryStream())
                using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Encoder = options.Encoder })) {
                    jsonDocument.WriteTo(writer);
                    writer.Flush();
                    json = Encoding.UTF8.GetString(stream.ToArray());
                }
    
                // deserialize the JSON to the type specified by $type
                try {
                    return (TBase)JsonSerializer.Deserialize(json, NameToType(typeName), options);
                }
                catch (Exception ex) {
                    throw new InvalidOperationException("Invalid JSON in request.", ex);
                }
            }
    
    
            public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options)
            {
                // create an ExpandoObject from the value to serialize so we can dynamically add a $type property to it
                ExpandoObject expando = ToExpandoObject(value);
                expando.TryAdd(TypePropertyName, TypeToName(value.GetType()));
    
                // serialize the expando
                JsonSerializer.Serialize(writer, expando, options);
            }
    
    
            private static ExpandoObject ToExpandoObject(object obj)
            {
                var expando = new ExpandoObject();
                if (obj != null) {
                    // copy all public properties
                    foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead)) {
                        expando.TryAdd(property.Name, property.GetValue(obj));
                    }
                }
    
                return expando;
            }
        }
    

    2) For each of your base types, create a class that derives from DerivedTypeJsonConverter. Implement the 2 abstract methods which are for mapping $type strings to actual types. Here is an example for my IResultProperty interface that you can follow.

        public class ResultPropertyJsonConverter : DerivedTypeJsonConverter<IResultProperty>
        {
            protected override Type NameToType(string typeName)
            {
                return typeName switch
                {
                    // map string values to types
                    nameof(StringResultProperty) => typeof(StringResultProperty)
    
                    // TODO: Create a case for each derived type
                };
            }
    
            protected override string TypeToName(Type type)
            {
                // map types to string values
                if (type == typeof(StringResultProperty)) return nameof(StringResultProperty);
    
                // TODO: Create a condition for each derived type
            }
        }
    

    3) Register the converters in Startup.cs.

        services.AddControllers()
            .AddJsonOptions(options => {
                options.JsonSerializerOptions.Converters.Add(new ResultPropertyJsonConverter());
    
                // TODO: Add each converter
            });
    

    4) In requests to the API, objects of derived types will need to include a $type property. Example JSON: { "Value":"Hi!", "$type":"StringResultProperty" }

    Full gist here

    0 讨论(0)
  • 2021-02-13 03:16

    While the other answers are good and solves the problem, if all you want is the general behavior to be like pre netcore3, you can use the Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package and in Startup.cs do:

    services.AddControllers().AddNewtonsoftJson()
    

    More info here. This way, you don't need to create any extra json-converters.

    0 讨论(0)
  • 2021-02-13 03:20

    I solved it by writing this extension:

    public static class JsonSerializationExtensions
    {
        public static string ToJson<T>(this IEnumerable<T> enumerable, bool includeDerivedTypesProperties = true)
                where T : class
        {
            var jsonOptions = new JsonSerializerOptions()
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };
    
            if (includeDerivedTypeProperties)
            {
                var collection = enumerable.Select(e => e as object).ToList();
                return JsonSerializer.Serialize<object>(collection, jsonOptions);
            }
            else
            {
                return JsonSerializer.Serialize(enumerable, jsonOptions);
            }
        }
    }
    
    0 讨论(0)
  • 2021-02-13 03:21

    I was also struggling with this in a .NET Core 3.1 API, where I wanted the result to include $type attribute.

    As suggested, install the correct package and then 'AddNewtonsoftJson'.

    I wanted the $type field to be added to show the derived type handling, to get that

    services.AddControllers().AddNewtonsoftJson(options => 
    { 
        options.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All;
    });
    
    0 讨论(0)
提交回复
热议问题