Florian Patzl
Florian Patzl

Reputation: 333

Can Springfox 3 generate Integer boundaries based on JSR 303 @Min/@Max in OAS3 API docs?

I created a Spring Boot application with a RestController that validates the DTO passed to a POST method based on JSR 303 validation annotations. API docs are generated using Springfox.

Validations are applied correctly and show up in the OAS2 API Docs. They are incomplete in the OAS3 API docs however - no minimum/maximum boundaries are generated for the Integer fields.

I'm using Spring Boot 2.5.2 (this is important as the latest version, 2.6.2, has issues with Springfox) and Springfox 3.0.0.

Since I found no specific hints in documentation + Springfox issue tracking and JSR303 support is working for the most part, I think this is a bug or oversight in Springfox OAS3 support. In the meantime I found a workaround which I will post as an answer - if I missed anything or there are better solutions I'd be happy to hear about that.

Details:

Controller

@Slf4j
@RestController
public class MyController {

    @PostMapping
    public void send(@Valid @RequestBody MyDTO dto) {
        log.info("{}", dto);
    }
}

DTO

@Value
public class MyDTO {
    @Size(max = 200)
    String text;

    @Max(2)
    @Min(1)
    Integer number;

    @Max(4)
    @Min(3)
    int number2;

    @Max(6)
    @Min(5)
    BigDecimal decimal;
}

OAS2 DTO schema (extracted from http://localhost:8080/v2/api-docs)

{
  "MyDTO": {
    "type": "object",
    "properties": {
      "decimal": {
        "type": "number",
        "minimum": 5,
        "maximum": 6,
        "exclusiveMinimum": false,
        "exclusiveMaximum": false
      },
      "number": {
        "type": "integer",
        "format": "int32",
        "minimum": 1,
        "maximum": 2,
        "exclusiveMinimum": false,
        "exclusiveMaximum": false
      },
      "number2": {
        "type": "integer",
        "format": "int32",
        "minimum": 3,
        "maximum": 4,
        "exclusiveMinimum": false,
        "exclusiveMaximum": false
      },
      "text": {
        "type": "string",
        "minLength": 0,
        "maxLength": 200
      }
    },
    "title": "MyDTO"
  }
}

OAS3 DTO schema (extracted from http://localhost:8080/v3/api-docs)

{
  "schemas": {
    "MyDTO": {
      "title": "MyDTO",
      "type": "object",
      "properties": {
        "decimal": {
          "maximum": 6,
          "exclusiveMaximum": false,
          "minimum": 5,
          "exclusiveMinimum": false,
          "type": "number",
          "format": "bigdecimal"
        },
        "number": {
          "type": "integer",
          "format": "int32"
        },
        "number2": {
          "type": "integer",
          "format": "int32"
        },
        "text": {
          "maxLength": 200,
          "minLength": 0,
          "type": "string"
        }
      }
    }
  }
}

Upvotes: 0

Views: 297

Answers (1)

Florian Patzl
Florian Patzl

Reputation: 333

After debugging Springfox I learned that the class springfox.documentation.oas.mappers.SchemaMapper in springfox-oas converts a "general model" to a format for OAS3. In the "general model", field boundaries are represented by an "NumericElementFacet". A specific property that is being mapped is a subclass of "Schema".

The problem seems to happen here: https://github.com/springfox/springfox/blob/bc9d0cad83e5dfdb30ddb487594fbc33fc1ba28c/springfox-oas/src/main/java/springfox/documentation/oas/mappers/SchemaMapper.java#L385

Properties represented by a "NumberSchema" are handled correctly (e.g. BigDecimal), their boundaries from the "NumericElementFacet" are applied. Integer fields (and further tests have shown: also Short and Long) are however represented by "IntegerSchema", which isn't handled there so the boundaries are not applied to the resulting API.

So what I did as a workaround was subclassing SchemaMapper, post-processing the results of mapProperties and registering the subclass as @Primary to override the springfox component:

import io.swagger.v3.oas.models.media.Schema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.oas.mappers.*;
import springfox.documentation.schema.*;
import springfox.documentation.service.ModelNamesRegistry;

import java.util.*;

@Primary
@Component
@Slf4j
public class IntegerBoundarySupportingOasSchemaMapper extends SchemaMapper {
    @Override
    @SuppressWarnings("rawtypes")
    protected Map<String, Schema> mapProperties(
            Map<String, PropertySpecification> properties,
            ModelNamesRegistry modelNamesRegistry) {
        var result = super.mapProperties(properties, modelNamesRegistry);

        result.values()
                .stream()
                // "integer" seems to cover at least Java Short, Integer and Long.
                .filter(property -> "integer".equals(property.getType()))
                .forEach(property -> properties.get(property.getName())
                        .getFacets()
                        .stream()
                        .filter(NumericElementFacet.class::isInstance)
                        .map(NumericElementFacet.class::cast)
                        .findFirst()
                        .ifPresent(f -> {
                            log.trace("Adding boundaries to API field {} (min={}, max={})",
                                    property.getName(),
                                    f.getMinimum(),
                                    f.getMaximum());

                            property.setMaximum(f.getMaximum());
                            property.exclusiveMaximum(f.getExclusiveMaximum());
                            property.setMinimum(f.getMinimum());
                            property.exclusiveMinimum(f.getExclusiveMinimum());
                        }));

        return result;
    }
}

In my case, this is working fine, so maybe its helping someone else, too.

Side note: The SchemaMapper is called every time I retrieve http://localhost:8080/v3/api-docs, that might be something to keep in mind when considering other time-consuming modifications of the schema.

Upvotes: 1

Related Questions