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

OpenAPI 3 | Support combined request/reponse models using readOnly/writeOnly and required #4306

Open tore-hammervoll opened 1 year ago

tore-hammervoll commented 1 year ago

OpenAPI: Read-Only and Write-Only Properties

You can use the readOnly and writeOnly keywords to mark specific properties as read-only or write-only. This is useful, for example, when GET returns more properties than used in POST – you can use the same schema in both GET and POST and mark the extra properties as readOnly. readOnly properties are included in responses but not in requests, and writeOnly properties may be sent in requests but not in responses.

As far as I can tell, readOnly: true and writeOnly: true is ignored during code generation.

My use case is to generate working request and response models for clients and controllers from a single OpenAPI specification. This is in a contract-first development workflow. In order to reduce the number of nearly identical schemas, I annotate properties with readOnly: true if a property is only part of the response but not valid in requests. readOnly would then for instance be a resource id or some other property that is generated upon creation. These properties can then also be marked as required to indicate that they will always be present in a response.

Redoc will render these schemas as expected, with the property marked as required in responses but not listed at all in the request.

NSwag will by default generated a single model class with System.ComponentModel.DataAnnotations.Required and Json "required" annotions for STJ or Newtonsoft for any property that is required, ignoring readOnly and writeOnly. This makes the generated class unusable as a request model in a controller with input validation.

One simple way I see to support this is probably to generate separate classes for the request and response if any properties are marked as readOnly or writeOnly.

If there is a way to generate a single class that can be used for request and response in controllers with validation and in clients, that would be ideal. Maybe this is possible with separate serializer and deserializer settings for client and server, if STJ or Newtonsoft supports this. However, I am not sure how the DataAnnotations validation side of this could work.

dellch commented 6 months ago

I have a similar issue, possibly related, concerning the use of allOf: and the required property (but not readOnly or writeOnly. Example:

requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: The POST body
              allOf: 
                - $ref: '#/components/schemas/Pet'
                - type: object
                  required: 
                    - name
                    - tag

Using this Pet schema:

  schemas:
    Pet:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string

This is rendered correctly in Swagger UI, and Redoc: image

But the generated C# class, does not include these requirements:

    /// <summary>
    /// The POST body
    /// </summary>
    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class Body : Pet
    {

    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class Pet
    {
        [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public long? Id { get; set; }

        [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Name { get; set; }

        [Newtonsoft.Json.JsonProperty("tag", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Tag { get; set; }

        private System.Collections.Generic.IDictionary<string, object> _additionalProperties;

        [Newtonsoft.Json.JsonExtensionData]
        public System.Collections.Generic.IDictionary<string, object> AdditionalProperties
        {
            get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary<string, object>()); }
            set { _additionalProperties = value; }
        }

    }