How to implement OpenAPI readOnly and writeOnly with Swashbuckle
Asked Answered
S

2

7

I'm using Swashbuckle (6.1.1) in a .Net 5.0 Web API. I'm still learning, but I'd like to implement a class where certain properties only are valid when 'reading' with a GET, and other properties when 'writing' with a POST. According to the OpenAPI spec:

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.

This is exactly what I want to achieve. However, I'm struggling to get Swashbuckle to generate the OpenAPI spec with the readOnly and writeOnly keyworks.

For example:

    public class testDetails
    {            
        public string commonProperty { get; set; }           
        public  string readOnlyProperty { get; set; }
        public string writeOnlyProperty {  get; set; }
    }

    [ProducesResponseType(StatusCodes.Status200OK)]    
    [HttpGet("Get")]
    public IActionResult Get([FromQuery] testDetails details)
    {
        Debug.WriteLine($"commonProperty is {details.commonProperty}");
        Debug.WriteLine($"readOnlyProperty is {details.readOnlyProperty}");
        Debug.WriteLine($"writeOnlyProperty is {details.writeOnlyProperty}");
        return Ok();
    }

I'd like readOnlyProperty to be tagged as readOnly, and writeOnlyProperty to be tagged as writeOnly in the generated swagger.json.

In effect, writeOnlyProperty shouldn't appear as a property for any GET (but would appear for a POST/PUT), and conversely readOnlyProperty should be available for a GET but not a POST.

I've tried adding a System.ComponentModel [ReadOnly] attribute, but with no effect. I've also tried changing the accessors to

 public class testDetails
    {            
        public string commonProperty { get; set; }           
        public  string readOnlyProperty { get; internal set; }
        public string writeOnlyProperty {  internal get; set; }
    }

... but this just ends up hiding the properties completely. None of this affects the actual operation of the code, but I'd still like properties to only be writable where they need to be, and read-only otherwise - exactly as the OpenAPI spec describes. Is there a way to do this, without creating separate "read and write classes"?

Strangulation answered 7/4, 2021 at 18:56 Comment(0)
E
9

You can annotate read-only and write-only properties with SwaggerSchemaAttribute from the package Swashbuckle.AspNetCore.Annotations. This will allow you to generate OpenAPI spec with the readOnly and writeOnly keywords and also hide properties from Swagger UI.

Follow these steps:

  1. Install the following Nuget package into your ASP.NET Core application.
Install-Package Swashbuckle.AspNetCore.Annotations
  1. In the ConfigureServices method of Startup.cs, enable annotations within in the Swagger config block:
services.AddSwaggerGen(c =>
{
   ...

   c.EnableAnnotations();
});
  1. Add attributes to your models:
public class TestDetails
{
    public string CommonProperty { get; set; }

    [SwaggerSchema(ReadOnly = true)]
    public string ReadOnlyProperty { get; set; }

    [SwaggerSchema(WriteOnly = true)]
    public string WriteOnlyProperty { get; set; }
}

Your controller may look like:

[ApiController]
[Route("[controller]")]
public class DataController : ControllerBase
{
    private TestDetails testDetails = new TestDetails()
    {
        CommonProperty = "Common prop value",
        ReadOnlyProperty = "ReadOnly prop value",
        WriteOnlyProperty = "WriteOnly prop value"
    };

    public DataController()
    {
        
    }

    [HttpGet]
    [ProducesResponseType(typeof(TestDetails), (int) HttpStatusCode.OK)]
    public IActionResult Get()
    {
        return Ok(testDetails);
    }

    [HttpPost]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    public IActionResult Post([FromBody] TestDetails details)
    {
        return Ok();
    }
}
Exarch answered 15/4, 2021 at 13:41 Comment(1)
One thing to note: if you have a property marked as required, that will prevent this readonly annotation from having any effectKohlrabi
F
0

I was not able to include Swashbuckle.AspNetCore.Annotations as a dependency on my DTO classes, so I configured it with an implementation of ISchemaFilter instead. Note: my JSON is serialised as camelCase.

using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;


public class CustomSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        var matchedReadonlyConfig = PropertiesMappedAsReadOnly.FirstOrDefault(x => x.specifiedClass == context.Type
                && x.specifiedPropertyName == property.Name);

            // Mark the property as readonly in the schema
            if (matchedReadonlyConfig != default)
            {
                string formattedPropertyName = System.Text.Json.JsonNamingPolicy.CamelCase.ConvertName(property.Name);
                schema.Properties[formattedPropertyName].ReadOnly = true;
            }

        }
    }

    private static List<(Type specifiedClass, string specifiedPropertyName)> PropertiesMappedAsReadOnly
    {
        get
        {
            {
                return new List<(Type, string)>() {
                    (typeof(TestDetails), nameof(TestDetails.ReadOnlyProperty)),
                    (typeof(Foo), nameof(Foo.FooFoo)),
                    (typeof(Bar), nameof(Bar.FooFoo))
                };
            }
        }
    }
}

And then in Program.cs or Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });

        // Register the custom schema filter
        c.SchemaFilter<CustomSchemaFilter>();
    });
}
Faulk answered 14/6 at 5:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.