Home > Software engineering >  Despite Open API specification ASP.NET sees every property as mandatory
Despite Open API specification ASP.NET sees every property as mandatory

Time:04-04

I normally generate the server code directly in https://swagger.io/ online service

Going on I decided to add more automation so I decided to use the CLI tool with aspnetcore generator: https://openapi-generator.tech/docs/generators/aspnetcore

This is the important part of the PowerShell script that triggers the generator:

"openapi-generator-cli generate -g aspnetcore -i $ymlPath -o $outPath "   `
    "--additional-properties=`"packageName=$projectName,nullableReferenceTypes=true,operationResultTask=true,"  
    "operationModifier=$opModifier,generateBody=$generateBody,aspnetCoreVersion=5.0`"" | iex

YML part with model schema:

Profile:
  required: [ walletAddress, publicName ]
  properties:
    walletAddress:
      type: string
      example: '0x008F7c856B71190C6E44A51a30A4ec32F68545e0'
    type:
      type: string
      example: 'itemtype'
    id:
      type: string
      example: 'pre_LfeolJ7DINWPTmQzArvT'
    createdAt:
      type: string
      format: date-time
      example: '2022-03-29T16:59:22.9033559Z'  
    publicName:
      type: string
    publicSlugUrl:
      type: string
      format: url
    emailAddress:
      type: string
      format: email
    phoneNumber:
      type: string
    location:
      type: string
    about:
      type: string

Generated C#:

namespace XxxxXxxxx.WebApi.Models
{ 
    [DataContract]
    public partial class Profile : IEquatable<Profile>
    {
        [Required]
        [DataMember(Name="walletAddress", EmitDefaultValue=false)]
        public string WalletAddress { get; set; }

        [DataMember(Name="type", EmitDefaultValue=false)]
        public string Type { get; set; }

        [DataMember(Name="id", EmitDefaultValue=false)]
        public string Id { get; set; }

        [DataMember(Name="createdAt", EmitDefaultValue=false)]
        public DateTime? CreatedAt { get; set; }

        [Required]
        [DataMember(Name="publicName", EmitDefaultValue=false)]
        public string PublicName { get; set; }

        [DataMember(Name="publicSlugUrl", EmitDefaultValue=false)]
        public string PublicSlugUrl { get; set; }

        [DataMember(Name="emailAddress", EmitDefaultValue=false)]
        public string EmailAddress { get; set; }

        [DataMember(Name="phoneNumber", EmitDefaultValue=false)]
        public string PhoneNumber { get; set; }

        [DataMember(Name="location", EmitDefaultValue=false)]
        public string Location { get; set; }


        [DataMember(Name="about", EmitDefaultValue=false)]
        public string About { get; set; }


        public override string ToString() { //...
        }

        public string ToJson()
        {
            return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented);
        }

        public override bool Equals(object obj)
        {
            if (obj is null) return false;
            if (ReferenceEquals(this, obj)) return true;
            return obj.GetType() == GetType() && Equals((Profile)obj);
        }

        public bool Equals(Profile other)  { //...
        }

        public override int GetHashCode( { //...
        }

        #region Operators
        // ...
        #endregion Operators
    }
}

JSON verison of OpenAPI model:

"Profile" : {
  "example" : {
    "createdAt" : "2022-03-29T16:59:22.9033559Z",
    "emailAddress" : "[email protected]",
    "phoneNumber" : " 6563297537",
    "publicName" : "Onxy DAO",
    "about" : "about",
    "publicSlugUrl" : "https://chain.party/onxy_dao",
    "location" : "Germany",
    "id" : "pre_LfeolJ7DINWPTmQzArvT",
    "walletAddress" : "0x008F7c856B71190C6E44A51a30A4ec32F68545e0",
    "type" : "itemtype"
  },
  "properties" : {
    "walletAddress" : {
      "example" : "0x008F7c856B71190C6E44A51a30A4ec32F68545e0",
      "type" : "string"
    },
    "type" : {
      "example" : "itemtype",
      "type" : "string"
    },
    "id" : {
      "example" : "pre_LfeolJ7DINWPTmQzArvT",
      "type" : "string"
    },
    "createdAt" : {
      "example" : "2022-03-29T16:59:22.9033559Z",
      "format" : "date-time",
      "type" : "string"
    },
    "publicName" : {
      "type" : "string"
    },
    "publicSlugUrl" : {
      "format" : "url",
      "type" : "string"
    },
    "emailAddress" : {
      "format" : "email",
      "type" : "string"
    },
    "phoneNumber" : {
      "type" : "string"
    },
    "location" : {
      "type" : "string"
    },
    "about" : {
      "type" : "string"
    }
  },
  "required" : [ "publicName", "walletAddress" ]
}

I should be able to post this payload with only the two required fields, bit i get a validation error on about all other fields:

{
    "errors": {
        "id": [
            "The Id field is required."
        ],
        "about": [
            "The About field is required."
        ],
        "location": [
            "The Location field is required."
        ],
        "publicName": [
            "The PublicName field is required."
        ],
        "phoneNumber": [
            "The PhoneNumber field is required."
        ],
        "emailAddress": [
            "The EmailAddress field is required."
        ],
        "publicSlugUrl": [
            "The PublicSlugUrl field is required."
        ]
    },
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-42995fb014cad1fbb999645cb61e4cf9-ade43e222f9917ae-00"
}

The app is configured in this way:

using System.Reflection;
using ChainParty.WebApi.Filters;
using ChainParty.WebApi.Formatters;
using ChainParty.WebApi.OpenApi;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services
    .AddMvc(opts => opts.InputFormatters.Insert(0, new InputFormatterStream()))
        .ConfigureApplicationPartManager(apm => {
            var originals = apm.FeatureProviders.OfType<ControllerFeatureProvider>().ToList();
            foreach (var original in originals)
                apm.FeatureProviders.Remove(original);
            apm.FeatureProviders.Add(new DefaultControllerFeatureProvider());
        })
        .AddNewtonsoftJson(opts => {
            opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            opts.SerializerSettings.Converters.Add(new StringEnumConverter
            {
                NamingStrategy = new CamelCaseNamingStrategy()
            });
        });


// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services
    .AddSwaggerGen(c => {
        c.SwaggerDoc("2.0.0", new OpenApiInfo
        {
            Title = "ChainParty",
            Description = "ChainParty (ASP.NET Core 3.1)",
            TermsOfService = new Uri("https://github.com/openapitools/openapi-generator"),
            Contact = new OpenApiContact
            {
                Name = "OpenAPI-Generator Contributors",
                Url = new Uri("https://github.com/openapitools/openapi-generator"),
                Email = "[email protected]"
            },
            License = new OpenApiLicense
            {
                Name = "NoLicense",
                Url = new Uri("http://localhost")
            },
            Version = "2.0.0",
        });
        c.CustomSchemaIds(type => type.FriendlyId(true));
        c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{Assembly.GetEntryAssembly().GetName().Name}.xml");

        // Include DataAnnotation attributes on Controller Action parameters as OpenAPI validation rules (e.g required, pattern, ..)
        // Use [ValidateModelState] on Actions to actually validate it in C# as well!
        c.OperationFilter<GeneratePathParamsValidationFilter>();
    });
builder.Services
        .AddSwaggerGenNewtonsoftSupport();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
    app.UseDeveloperExceptionPage();
    app.UseSwagger(c => {
        c.RouteTemplate = "openapi/{documentName}/openapi.json";
    })
    .UseSwaggerUI(c => {
        // set route prefix to openapi, e.g. http://localhost:8080/openapi/index.html
        c.RoutePrefix = "openapi";
        //TODO: Either use the SwaggerGen generated OpenAPI contract (generated from C# classes)
        c.SwaggerEndpoint("/openapi/2.0.0/openapi.json", "ChainParty");

        //TODO: Or alternatively use the original OpenAPI contract that's included in the static files
        // c.SwaggerEndpoint("/openapi-original.json", "ChainParty Original");
    });
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.UseRouting();
app.MapControllers();

app.Run();

The only customization is the controllers name resolution.

Any help really appreciated, best regards

CodePudding user response:

As this document said:

Beginning with .NET 6, new projects include the <Nullable>enable</Nullable> element in the project file. Once the feature is turned on, existing reference variable declarations become non-nullable reference types.

In .NET 6 the non-nullable property must be required, otherwise the ModelState will be invalid.

To achieve your requirement, you can remove <Nullable>enable</Nullable> from your project file.

Another way is that you can add ? to allow nullable:

[DataContract]
public partial class Profile : IEquatable<Profile>
{
    [Required]
    [DataMember(Name="walletAddress", EmitDefaultValue=false)]
    public string WalletAddress { get; set; }

    [DataMember(Name="type", EmitDefaultValue=false)]
    public string Type { get; set; }

    [DataMember(Name="id", EmitDefaultValue=false)]
    public string? Id { get; set; }

    [DataMember(Name="createdAt", EmitDefaultValue=false)]
    public DateTime? CreatedAt { get; set; }

    [Required]
    [DataMember(Name="publicName", EmitDefaultValue=false)]
    public string? PublicName { get; set; }
    //other proerpties..
}
  • Related