OpenAPITools / OpenAPITools/openapi-generator

[REQ] [csharp-netcore] Complete nullable reference types question / request

Open
#15,556 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Enhancement: Feature
Dominant language
Java
Stars
26.8k
Forks
7.7k
PR merge metrics
PR metrics pending

Description

Is your feature request related to a problem? Please describe.

When using the 'csharp-netcore' generator even with nullableReferenceTypes set to true and targetting only net6.0 / net7.0 nullable reference/value types in generated models do not use nullable annotations.
This leads projects referencing generated models and assigning null values to nullable elements generating warnings.
I don't know if I'm doing something wrong or if this is not currently supported by the generator.

Describe the solution you'd like

When nullableReferenceTypes is set to true the reference types set to nullable or not required should have nullable annotations.
A new config option nullableValueTypes could also be added to add a nullable annotation to value types in models differently from reference types.

Describe alternatives you've considered

Disable nullable context warnings in projects referencing generated models assigning null values.
Currently working on custom overriding model templates as it seems from what I saw the generator just does not support this currently.

Additional context

If I didn't miss something and this is something currently not supported by the generator I can work on a new pull request supporting it.

Reproducible example

OpenApi definition (openapi.yaml):

openapi: 3.0.3
info:
  description: |
    An Open API description
  title: A title
  version: 0.1.0
servers:
- url: https://example.com
tags:
- description: All authentication related routes
  name: Authentication
paths:
  /auth/oauth/v2/token:
    post:
      description: Retrieve a Bearer token from a client id / client secret
      operationId: PostAuthOauthV2Token
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AuthenticationBody'
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BearerToken'
          description: Bearer token returned
      security: []
      servers:
      - url: https://auth.example.com
      summary: /auth/oauth/v2/token - Retrieve a Bearer token
      tags:
      - Authentication
components:
  schemas:
    AuthenticationBody:
      properties:
        client_id:
          type: string
        client_secret:
          type: string
        grant_type:
          type: string
      required:
      - client_id
      - client_secret
      - grant_type
      type: object
    BearerToken:
      properties:
        access_token:
          type: string
        token_type:
          type: string
        expires_in:
          type: integer
          nullable: true
        scope:
          type: string
          nullable: true
        nullableSubobject:
          nullable: true
          allOf: 
            - $ref: '#/components/schemas/SubObject'
      required:
      - access_token
      - token_type
      type: object
    SubObject:
      properties:
        aProp:
          type: string
          nullable: true
        bProp:
          type: integer
          nullable: true
      type: object
  securitySchemes:
    bearerAuth:
      scheme: bearer
      type: http

Generated BearerToken class

/*
 * A title
 *
 * An Open API description 
 *
 * The version of the OpenAPI document: 0.1.0
 * Generated by: https://github.com/openapitools/openapi-generator.git
 */


using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using System.ComponentModel.DataAnnotations;
using OpenAPIDateConverter = OpenApiClient.OpenAPIDateConverter;

namespace OpenApiClient.Model
{
    /// <summary>
    /// BearerToken
    /// </summary>
    [DataContract(Name = "BearerToken")]
    public partial class BearerToken : IEquatable<BearerToken>, IValidatableObject
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="BearerToken" /> class.
        /// </summary>
        [JsonConstructorAttribute]
        protected BearerToken() { }
        /// <summary>
        /// Initializes a new instance of the <see cref="BearerToken" /> class.
        /// </summary>
        /// <param name="accessToken">accessToken (required).</param>
        /// <param name="tokenType">tokenType (required).</param>
        /// <param name="expiresIn">expiresIn.</param>
        /// <param name="scope">scope.</param>
        /// <param name="nullableSubobject">nullableSubobject.</param>
        public BearerToken(string accessToken = default(string), string tokenType = default(string), int? expiresIn = default(int?), string scope = default(string), SubObject nullableSubobject = default(SubObject))
        {
            // to ensure "accessToken" is required (not null)
            if (accessToken == null)
            {
                throw new ArgumentNullException("accessToken is a required property for BearerToken and cannot be null");
            }
            this.AccessToken = accessToken;
            // to ensure "tokenType" is required (not null)
            if (tokenType == null)
            {
                throw new ArgumentNullException("tokenType is a required property for BearerToken and cannot be null");
            }
            this.TokenType = tokenType;
            this.ExpiresIn = expiresIn;
            this.Scope = scope;
            this.NullableSubobject = nullableSubobject;
        }

        /// <summary>
        /// Gets or Sets AccessToken
        /// </summary>
        [DataMember(Name = "access_token", IsRequired = true, EmitDefaultValue = true)]
        public string AccessToken { get; set; }

        /// <summary>
        /// Gets or Sets TokenType
        /// </summary>
        [DataMember(Name = "token_type", IsRequired = true, EmitDefaultValue = true)]
        public string TokenType { get; set; }

        /// <summary>
        /// Gets or Sets ExpiresIn
        /// </summary>
        [DataMember(Name = "expires_in", EmitDefaultValue = true)]
        public int? ExpiresIn { get; set; }

        /// <summary>
        /// Gets or Sets Scope
        /// </summary>
        [DataMember(Name = "scope", EmitDefaultValue = true)]
        public string Scope { get; set; }

        /// <summary>
        /// Gets or Sets NullableSubobject
        /// </summary>
        [DataMember(Name = "nullableSubobject", EmitDefaultValue = true)]
        public SubObject NullableSubobject { get; set; }

        /// <summary>
        /// Returns the string presentation of the object
        /// </summary>
        /// <returns>String presentation of the object</returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("class BearerToken {\n");
            sb.Append("  AccessToken: ").Append(AccessToken).Append("\n");
            sb.Append("  TokenType: ").Append(TokenType).Append("\n");
            sb.Append("  ExpiresIn: ").Append(ExpiresIn).Append("\n");
            sb.Append("  Scope: ").Append(Scope).Append("\n");
            sb.Append("  NullableSubobject: ").Append(NullableSubobject).Append("\n");
            sb.Append("}\n");
            return sb.ToString();
        }

        /// <summary>
        /// Returns the JSON string presentation of the object
        /// </summary>
        /// <returns>JSON string presentation of the object</returns>
        public virtual string ToJson()
        {
            return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented);
        }

        /// <summary>
        /// Returns true if objects are equal
        /// </summary>
        /// <param name="input">Object to be compared</param>
        /// <returns>Boolean</returns>
        public override bool Equals(object input)
        {
            return this.Equals(input as BearerToken);
        }

        /// <summary>
        /// Returns true if BearerToken instances are equal
        /// </summary>
        /// <param name="input">Instance of BearerToken to be compared</param>
        /// <returns>Boolean</returns>
        public bool Equals(BearerToken input)
        {
            if (input == null)
            {
                return false;
            }
            return 
                (
                    this.AccessToken == input.AccessToken ||
                    (this.AccessToken != null &&
                    this.AccessToken.Equals(input.AccessToken))
                ) && 
                (
                    this.TokenType == input.TokenType ||
                    (this.TokenType != null &&
                    this.TokenType.Equals(input.TokenType))
                ) && 
                (
                    this.ExpiresIn == input.ExpiresIn ||
                    (this.ExpiresIn != null &&
                    this.ExpiresIn.Equals(input.ExpiresIn))
                ) && 
                (
                    this.Scope == input.Scope ||
                    (this.Scope != null &&
                    this.Scope.Equals(input.Scope))
                ) && 
                (
                    this.NullableSubobject == input.NullableSubobject ||
                    (this.NullableSubobject != null &&
                    this.NullableSubobject.Equals(input.NullableSubobject))
                );
        }

        /// <summary>
        /// Gets the hash code
        /// </summary>
        /// <returns>Hash code</returns>
        public override int GetHashCode()
        {
            unchecked // Overflow is fine, just wrap
            {
                int hashCode = 41;
                if (this.AccessToken != null)
                {
                    hashCode = (hashCode * 59) + this.AccessToken.GetHashCode();
                }
                if (this.TokenType != null)
                {
                    hashCode = (hashCode * 59) + this.TokenType.GetHashCode();
                }
                if (this.ExpiresIn != null)
                {
                    hashCode = (hashCode * 59) + this.ExpiresIn.GetHashCode();
                }
                if (this.Scope != null)
                {
                    hashCode = (hashCode * 59) + this.Scope.GetHashCode();
                }
                if (this.NullableSubobject != null)
                {
                    hashCode = (hashCode * 59) + this.NullableSubobject.GetHashCode();
                }
                return hashCode;
            }
        }

        /// <summary>
        /// To validate all properties of the instance
        /// </summary>
        /// <param name="validationContext">Validation context</param>
        /// <returns>Validation Result</returns>
        IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
        {
            yield break;
        }
    }

}

What I expected (truncated to show only changed elements):

       /* ... */

        public BearerToken(string accessToken = default(string), string tokenType = default(string), int? expiresIn = default(int?), string? scope = default(string?), SubObject? nullableSubobject = default(SubObject?))

       /* ... */
        
        /// <summary>
        /// Gets or Sets Scope
        /// </summary>
        [DataMember(Name = "scope", EmitDefaultValue = true)]
        public string? Scope { get; set; }

        /// <summary>
        /// Gets or Sets NullableSubobject
        /// </summary>
        [DataMember(Name = "nullableSubobject", EmitDefaultValue = true)]
        public SubObject? NullableSubobject { get; set; }

       /* ... */

Command used: openapi-generator-cli generate -i ./openapi.yaml -g csharp-netcore -o ./Autogenerated -c ./openapi-generator-config.json

Config files:

  • openapitools.json
{
  "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
  "spaces": 2,
  "generator-cli": {
    "version": "6.6.0"
  }
}
  • openapi-generator-config.json
{
  "globalProperties": {
    "apiTests": false,
    "modelTests": false
  },
  "apiName": "ApiName",
  "netCoreProjectFile": true,
  "nullableReferenceTypes": true,
  "packageName": "OpenApiClient",
  "packageVersion": "1.0.0",
  "targetFramework": "net6.0"
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the csharp-netcore generator's handling of the nullableReferenceTypes option and compare its output with the supplied OpenAPI example. Check how nullable reference and value types are represented in the generated BearerToken and SubObject models. Done means nullable schema properties receive the expected C# annotations without changing required properties.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
api
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.