programing tip

Json을 Asp.Net Web API의 파생 형식으로 역 직렬화

itbloger 2020. 12. 7. 07:54
반응형

Json을 Asp.Net Web API의 파생 형식으로 역 직렬화


모델과 일치 (또는 바인딩)하려는 json을 보내는 WebAPI의 메서드를 호출하고 있습니다.

컨트롤러에는 다음과 같은 방법이 있습니다.

public Result Post([ModelBinder(typeof(CustomModelBinder))]MyClass model);

'MyClass', 매개 변수로 주어지면 추상 클래스입니다. 전달 된 json 유형에 따라 올바른 상속 된 클래스가 인스턴스화되기를 바랍니다.

이를 달성하기 위해 사용자 지정 바인더를 구현하려고합니다. 문제는 (매우 기본적인 것인지는 모르겠지만 아무것도 찾을 수 없습니다) 요청에 포함 된 원시 Json (또는 더 나은 일종의 직렬화)을 검색하는 방법을 모릅니다.

내가 참조:

  • actionContext.Request.Content

그러나 모든 메서드는 비동기로 노출됩니다. 모델 생성을 컨트롤러 메서드에 전달하는 데 누가 적합한 지 모르겠습니다.

감사합니다!


사용자 지정 모델 바인더가 필요하지 않습니다. 또한 요청 파이프 라인에 대해 고민 할 필요가 없습니다.

이 다른 SO : 기본 클래스 개체 목록을 역 직렬화하기 위해 JSON.NET에서 사용자 지정 JsonConverter를 구현하는 방법을 살펴보십시오 . .

나는 이것을 동일한 문제에 대한 내 자신의 해결책의 기초로 사용했습니다.

JsonCreationConverter<T>해당 SO 에서 참조 된 것으로 시작합니다 (응답의 유형 직렬화 문제를 해결하기 위해 약간 수정 됨).

public abstract class JsonCreationConverter<T> : JsonConverter
{
    /// <summary>
    /// this is very important, otherwise serialization breaks!
    /// </summary>
    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }
    /// <summary> 
    /// Create an instance of objectType, based properties in the JSON object 
    /// </summary> 
    /// <param name="objectType">type of object expected</param> 
    /// <param name="jObject">contents of JSON object that will be 
    /// deserialized</param> 
    /// <returns></returns> 
    protected abstract T Create(Type objectType, JObject jObject);

    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType,
      object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        // Load JObject from stream 
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject 
        T target = Create(objectType, jObject);

        // Populate the object properties 
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }

    public override void WriteJson(JsonWriter writer, object value, 
      JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
} 

이제 유형에 주석을 달고 JsonConverterAttributeJson.Net을 사용자 지정 변환기로 지정할 수 있습니다 .

[JsonConverter(typeof(MyCustomConverter))]
public abstract class BaseClass{
  private class MyCustomConverter : JsonCreationConverter<BaseClass>
  {
     protected override BaseClass Create(Type objectType, 
       Newtonsoft.Json.Linq.JObject jObject)
     {
       //TODO: read the raw JSON object through jObject to identify the type
       //e.g. here I'm reading a 'typename' property:

       if("DerivedType".Equals(jObject.Value<string>("typename")))
       {
         return new DerivedClass();
       }
       return new DefaultClass();

       //now the base class' code will populate the returned object.
     }
  }
}

public class DerivedClass : BaseClass {
  public string DerivedProperty { get; set; }
}

public class DefaultClass : BaseClass {
  public string DefaultProperty { get; set; }
}

이제 기본 유형을 매개 변수로 사용할 수 있습니다.

public Result Post(BaseClass arg) {

}

그리고 우리가 게시한다면 :

{ typename: 'DerivedType', DerivedProperty: 'hello' }

그러면 arg의 인스턴스가 DerivedClass되지만 게시하면 다음과 같습니다.

{ DefaultProperty: 'world' }

그런 다음 DefaultClass.

편집-이 방법을 선호하는 이유 TypeNameHandling.Auto/All

나는 TypeNameHandling.Auto/AllJotaBe가지지하는 것을 사용하는 것이 항상 이상적인 해결책은 아니라고 생각합니다. 이 경우에 해당 될 수 있지만 개인적으로 다음이 아니면하지 않습니다.

  • 내 API는 나 또는 내 팀 에서만 사용할 것입니다.
  • 이중 XML 호환 엔드 포인트는 신경 쓰지 않습니다.

Json.Net TypeNameHandling.Auto또는 All이 사용되면 웹 서버가 형식 이름을 MyNamespace.MyType, MyAssemblyName.

I have said in comments that I think this is a security concern. Mention was made of this in some documentation I read from Microsoft. It's not mentioned any more, it seems, however I still feel it's a valid concern. I don't ever want to expose namespace-qualified type names and assembly names to the outside world. It's increasing my attack surface. So, yes, I can not have Object properties/parameters my API types, but who's to say the rest of my site is completely hole-free? Who's to say a future endpoint doesn't expose the ability to exploit type names? Why take that chance just because it's easier?

Also - if you are writing a 'proper' API, i.e. specifically for consumption by third-parties and not just for yourself, and you're using Web API, then you're most likely looking to leverage the JSON/XML content-type handling (as a minimum). See how far you get trying to write documentation that's easy to consume, which refers to all your API types differently for XML and JSON formats.

By overriding how JSON.Net understands the type names, you can bring the two into line, making the choice between XML/JSON for your caller purely based on taste, rather than because the type names are easier to remember in one or the other.


You don't need to implement it by yourself. JSON.NET has native support for it.

You have to specify the desired TypeNameHandling option for the JSON formatter, like this (in global.asax application start event):

JsonSerializerSettings serializerSettings = GlobalConfiguration.Configuration
   .Formatters.JsonFormatter.SerializerSettings;
serializerSettings.TypeNameHandling = TypeNameHandling.Auto;

If you specify Auto, like in the above sample, the parameter will be deserialized to the type specified in the $type property of the object. If the $type property is missing, it will be deserialized to the parameter's type. So you only have to specify the type when you're passing a parameter of a derived type. (This is the most flexible option).

For example, if you pass this parameter to a Web API action:

var param = {
    $type: 'MyNamespace.MyType, MyAssemblyName', // .NET fully qualified name
    ... // object properties
};

The parameter will be deserialized to an object of MyNamespace.MyType class.

This also works fro sub-properties, i.e., you can have an object like this, which specifies that an inner property is of a given type

var param = { 
   myTypedProperty: {
      $type: `...`
      ...
};

Here you can see a sample on JSON.NET documentation of TypeNameHandling.Auto.

This works at least since JSON.NET 4 release.

NOTE

You don't need to decorate anything with attirbutes, or do any other customization. It will work without any changes in your Web API code.

IMPORTANT NOTE

The $type must be the first property of the JSON serialized object. If not, it will be ignored.

COMPARISON TO CUSTOM JsonConverter/JsonConverterAttribute

I'm comparing the native solution to this answer.

To implement the JsonConverter/JsonConverterAttribute:

  • you need to implement a custom JsonConverter, and a custom JsonConverterAttribute
  • you need to use attributes to mark the parameters
  • you need to know beforehand the possible types expected for the parameter
  • you need to implement, or change the implementation of your JsonConverter whenever your types or properties change
  • there is a code smell of magic strings, to indicate the expected property names
  • you are not implementing something generic that can be used with any type
  • you're reinventing the wheel

In the author of the answer there's a comment regarding security. Unless you do something wrong (like accepting a too generic type for your parameter, like Object) there is no risk of getting an instance of the wrong type: JSON.NET native solution only instantiates an object of the parameter's type, or a type derived from it (if not, you get null).

And these are the advantages of JSON.NET native solution:

  • you don't need to implement anything (you only have to configure the TypeNameHandling once in your app)
  • you don't need to use attributes in your action parameters
  • you don't need to know the possible parameter types beforehand: you simply need to know the base type, and specify it in the parameter (it could be an abstract type, to make polymorphism more obvious)
  • the solution works for most cases (1) without changing anything
  • this solution is widely tested, and optimized
  • you don't need magic strings
  • the implementation is generic, and will accept any derived type

(1): if you want to receive parameter values that don't inherit from the same base type, this will not work, but I see no point on doing so

So I can't find any disadvantages, and find many advantages on JSON.NET solution.

WHY USING CUSTOM JsonConverter/JsonConverterAttribute

This is a good working solution that allows customization, that can be modified or extended to adapt it to your particular case.

If you want to do something that the native solution cannot do, like customizing the type names, or inferring the type of the parameter based on available property names, then do use this solution adapted to your own case. The other one cannot be customized, and will not work for your needs.


You can call async methods normally, your execution will be simply suspended until the method returns and you can return the model in standard manner. Just make a call like this:

string jsonContent = await actionContext.Request.Content.ReadAsStringAsync();

It will give you raw JSON.


If you want to use the TypeNameHandling.Auto but are concerned with security or dont like api consumers needing that level of behind the scenes knowledge you can handle the $type deserialize your self.

public class InheritanceSerializationBinder : DefaultSerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        switch (typeName)
        {
            case "parent[]": return typeof(Class1[]);
            case "parent": return typeof(Class1);
            case "child[]": return typeof(Class2[]);
            case "child": return typeof(Class2);
            default: return base.BindToType(assemblyName, typeName);
        }
    }
}

Then hook this up in global.asax.Application__Start

var config = GlobalConfiguration.Configuration;
        config.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings { Binder = new InheritanceSerializationBinder() };

finally i have used a wrapper class and [JsonProperty(TypeNameHandling = TypeNameHandling.Auto)] on a properpty containing the object with different types as i have not been able to get it to work by configuring the actual class.

This approach allows consumers to include the needed information in their request while allowing the documentation of the allowable values to be platform independent, easy to change, and easy to understand. All without having to write your own converster.

Credit to : https://mallibone.com/post/serialize-object-inheritance-with-json.net for showing me the custom deserializer of that field property.

참고URL : https://stackoverflow.com/questions/12638741/deserialising-json-to-derived-types-in-asp-net-web-api

반응형