RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.78k stars 1.29k forks source link

Remove NullValueHandling.Ignore from reference types #1129

Closed dmmusil closed 6 years ago

dmmusil commented 6 years ago

Is it possible to configure the Web API Assembly to C# client generation so that properties with reference types in the DTOs are not decorated with NullValueHandling.Ignore without having to decorate all my DTOs in the API with JsonRequired?

I want to validate the schema of objects I get through the client and the NullValueHandling is preventing them from appearing when they get serialized back to JSON even when I try to configure local serializer settings to include nulls.

dmmusil commented 6 years ago

I got it with a custom contract resolver.

public class CustomCamelCaseResolver : CamelCasePropertyNamesContractResolver
    {
        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            var property = base.CreateProperty(member, memberSerialization);

            property.NullValueHandling = NullValueHandling.Include;
            return property;
        }
    }
ghost commented 5 years ago

Could you please elaborate?

I see no way of specifying this at code-generation time, only how to override the behavior at the time when the (incorrectly) generated code is called.

dmmusil commented 5 years ago

In a partial implementation of ProcessResponse I store the response data from the API as a string so I have it in a raw format. Then I deserialize it using custom serializer settings to validate its schema. The generated code will still do it the way I don't want, but I work around that.

ghost commented 5 years ago

That is a seriously messed up way of "solving" this issue.

I discovered that using Swagger 2.0, there are only two constellations which NSwag's Codegen will generate:

It is possible using OpenAPI 3.0.0, but this will require porting my API spec to that language version (which is non-trivial) just to avoid a very arbitrary decision on NJsonSchema's part.

arckal commented 5 years ago

I got it with a custom contract resolver.

public class CustomCamelCaseResolver : CamelCasePropertyNamesContractResolver
    {
        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            var property = base.CreateProperty(member, memberSerialization);

            property.NullValueHandling = NullValueHandling.Include;
            return property;
        }
    }

Could you please explain in more detail, may be a sample code snippet could help.

dmmusil commented 5 years ago

It's been a while since I've looked at this code so I may not be remembering correctly.

I have NSwag generate a class called Client.cs. It generates it as a partial class with a bunch of partial methods. I create another partial implementation of UpdateJsonSerializerSettings() with this body:

        partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
        {
            settings.ContractResolver = new CustomCamelCaseResolver();
            settings.NullValueHandling = NullValueHandling.Include;
            settings.Converters = new List<JsonConverter> {new StringEnumConverter()};
            settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            settings.DefaultValueHandling = DefaultValueHandling.Include;
        }

It uses the resolver you quoted to make it act like I wanted. The thing I mentioned in a previous comment about messing with the raw json string was actually for another purpose.

arckal commented 5 years ago

Thanks, so where do I create and use this class "UpdateJsonSerializerSettings". I understand this name could be placed in NSwagStudio->CSharp Client tab (settings)-> JsonSerializerSettings?

but where do I'll put the code and how nswag will use it?

dmmusil commented 5 years ago

I created a new file called ClientPartial.cs in the same folder as the generated file, with a public partial class Client in it. In that partial class you can define a new implementation of the partial method from my last comment.

If you look in the constructor of the NSwag generated code, you can see where the partial method is called.

        public Client(System.Net.Http.HttpClient httpClient)
        {
            _httpClient = httpClient; 
            _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() => 
            {
                var settings = new Newtonsoft.Json.JsonSerializerSettings();
                UpdateJsonSerializerSettings(settings);
                return settings;
            });
        }
arckal commented 5 years ago

thanks again, I got it. I have included the client generation and found I could use settings constructor but even after initialized it in there, and using it while desalinizing it still not working for me, here are required items for your reference: This is a DTO generated by nswagstudio: [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "9.13.4.0 (Newtonsoft.Json v11.0.0.0)")] public partial class ExamResultLineItem { [Newtonsoft.Json.JsonProperty("key", Required = Newtonsoft.Json.Required.Always)] public int Key { get; set; }

    [Newtonsoft.Json.JsonProperty("examResultKey", Required = Newtonsoft.Json.Required.Always)]
    public int ExamResultKey { get; set; }

    [Newtonsoft.Json.JsonProperty("examDate", Required = Newtonsoft.Json.Required.Always)]
    [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
    public System.DateTime ExamDate { get; set; }

    [Newtonsoft.Json.JsonProperty("examType", Required = Newtonsoft.Json.Required.Always)]
    [System.ComponentModel.DataAnnotations.Required]
    public ExamType ExamType { get; set; } = new ExamType();

    [Newtonsoft.Json.JsonProperty("scoreType", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.**Ignore**)]
    public ScoreType ScoreType { get; set; }

    [Newtonsoft.Json.JsonProperty("administrationType", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.**Ignore**)]
    public AdministrationType AdministrationType { get; set; }

    [Newtonsoft.Json.JsonProperty("revision", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.**Ignore**)]
    public Revision Revision { get; set; }

    [Newtonsoft.Json.JsonProperty("score", Required = Newtonsoft.Json.Required.Always)]
    public double Score { get; set; }

}

---------------------------here is client class, my own modified separated class constructor----------------- public ApiClientService(string baseUrl) { _httpClient = new HttpClient(); BaseEndpoint = new Uri(basUrl); _settings = new Newtonsoft.Json.JsonSerializerSettings { NullValueHandling = NullValueHandling.Include, Converters = new List {new StringEnumConverter()}, //ReferenceLoopHandling = ReferenceLoopHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Include }; } ---------------- and here is how I am using it-------------------- public async Task PostAsync(string relativePath, T content) { AddHeaders(); var response = await _httpClient.PostAsync(CreateRequestUri(relativePath, ""), CreateHttpContent(content)); response.EnsureSuccessStatusCode(); var data = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(data, _settings); }