FastEndpoints / FastEndpoints

A light-weight REST API development framework for ASP.NET 6 and newer.
https://fast-endpoints.com
MIT License
4.74k stars 282 forks source link

Swagger schema definition For Endpoint Response Types Is not considering existing FluentValidators #692

Closed Yoztastic closed 6 months ago

Yoztastic commented 6 months ago

I have a request type for and Endpoint like

public record CompoundInstruction(AssetInstruction[] Instructions);

and an associated validator

public class CompoundInstructionValidator : Validator<CompoundInstruction>
{
  public CompoundInstructionValidator()
  {
    RuleFor(x => x.Instructions).NotNull();

    RuleForEach(x => x.Instructions).SetValidator(new AssetInstructionValidator());
  }
}

the Swagger document generated reflect this constraint

 "CompoundInstruction": {
    "type": "object",            
    "additionalProperties": false,
    "required": [
        "instructions"
    ],
    "properties": {
        "instructions": {
            "type": "array",
            "nullable": false,
            "items": {
                "$ref": "#/components/schemas/AssetInstruction"
            }
        },
        "metadata": {
            "$ref": "#/components/schemas/InstructionMetadata"
        }
    }
}

However, the response type for this endpoint

public record CompoundInstructionResponse(Guid RequestId, InstructionRecord[] Results);

public class CompoundInstructionResponseValidator : Validator<CompoundInstructionResponse>
{
  public CompoundInstructionResponseValidator()
  {
    RuleFor(x => x.Results).NotNull();
  }
}

Does not generate the desired swagger

"CompoundInstructionResponse": {
       "type": "object",
       "additionalProperties": false,
       "properties": {
           "requestId": {
               "type": "string",
               "format": "guid"
           },
           "results": {
               "type": "array",
               "items": {
                   "$ref": "#/components/schemas/InstructionRecord"
               }
           }
       }
   }

note there is no required array including results and "nullable": false, is absent.

It seems to me that FastEndpoints.EndpointDefinition has both Type requestDtoType, Type responseDtoType but only one property public Type? ValidatorType { get; internal set; }

And that FastEndpoints.Swagger.ValidationSchemaProcessor only gets a list of the validators associated with the Request Type hence the swagger generated for Response Type is under constrained.

It is quite important to include the fully constrained Swagger Schema since clients automate the JavaScript type definitions from this schema and they therefore get bloated with null checks that are redundant.

Is there a known work around to this?

dj-nitehawk commented 6 months ago

i believe this is the same thing as https://github.com/FastEndpoints/FastEndpoints/issues/388 so the solution is to do this:

.SwaggerDocument(o => o.DocumentSettings = d => d.MarkNonNullablePropsAsRequired());

which results in output like this:

    "components": {
        "schemas": {
            "CompoundInstructionResponse": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "requestId",
                    "results"
                ],
                "properties": { },
                    "results": { }
                    }
                }
            },
            "InstructionRecord": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "instruction"
                ],
                "properties": { }
                }

if that's still not constrained enough, you can just copy this nswag processor and add what you need.

as for infering the nullability/required info from validators, that's only supported for request DTOs as we've never seen anyone use validators for response DTOs before.

Yoztastic commented 6 months ago

Awesome!

So I think I need something like

public class MarkNonNullableArrayPropsAsRequired : ISchemaProcessor
{
  public void Process(SchemaProcessorContext context)
  {
    foreach (var (_, prop) in context.Schema.ActualProperties)
    {
      if (!prop.IsNullable(SchemaType.OpenApi3) && prop.IsArray)
      {
        prop.IsRequired = true;
        prop.IsNullableRaw = false;
      }
    }
  }
}

and just

 builder.Services.SwaggerDocument(options =>
  {
    options.DocumentSettings = s =>
    {
      s.MarkNonNullablePropsAsRequired();
      s.SchemaSettings.SchemaProcessors.Add(new MarkNonNullableArrayPropsAsRequired());
    };