Documenting custom marshmallow field types¶
flask-marshmallow-openapi
relies heavily on
apispec to generate type documentation.
apispec
itself works well with all marshmallow.Field
types and has documentation how
to add new ones.
Since flask-marshmallow-openapi
strongly encapsulates apispec
, instead of exposing
it to end users, OpenAPI
middleware implements wrappers for internal apispec
functionality. This is best explained by example.
Lets assume we want to introduce filed type representing ULID.
First, we need to create marshmallow.Field
class:
class UlidField(fields.String):
OPENAPI_SCHEMA_ATTRS = {
"type": "string",
"format": "ULID",
"examples": [
"01H4QG7EVN6J6HF5DAMS9TK53X",
"01H4QG7F1XB3MGW7GHB5PA4P89",
"01H4QG7F864HBXG63N8PBF1XBM",
"01H4QG7FEEW0D463QJEREF81P3",
"01H4QG7FMQCE8DAPV6EQZREDX4",
"01H4QG7FTZYHYM92AG6TG5ZDFR",
"01H4QG7G1898PRWKBAED26J0VE",
"01H4QG7G7H3DWQ671CV4PF6D62",
"01H4QG7GDSYJ0ZWF8J39XRAMR0",
"01H4QG7GM2WWEYQPAGM57MYK4Y",
"01H4QG7VE3BY903B5DNJ442RDN",
"01H4QG7WDCAWS1DN52EWWYXTG9",
"01H4QG7XCPGCPB3BYN0QPFX61R",
"01H4QG7XCPGCPB3BYN0QPFX61S",
"01H4QG7ZB859YDH4GXWY9V8V1H",
"01H4QG80AHN36H1X1R2FEG61V4",
"01H4QG819VCQFN67QXQCMRYD85",
"01H4QG8294DFA2S9CZTBGEPCNE",
"01H4QG838EMDFGRHN9AZW3SJGG",
"01H4QG847Q1KVSQE6F8GXKRTJH",
],
}
OPENAPI_URL_ID_PARAMETER = {
"name": "id",
"in": "path",
"required": True,
"allowEmptyValue": False,
"schema": {k: v for k, v in OPENAPI_SCHEMA_ATTRS.items() if k != "examples"},
}
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.metadata["format"] = self.OPENAPI_SCHEMA_ATTRS["format"]
self.validators.insert(0, ulid_validator)
RE_ULID = re.compile(r"[0-9A-HJKMNP-TV-Z]{26}")
def ulid_validator(value):
if not RE_ULID.match(value):
raise ValidationError("must be ULID encoded as Crockford's base32 string!")
Then, let’s add some schemas using it:
class PublisherSchema(ma.Schema):
id = UlidField()
name = ma.fields.String()
address = ma.fields.String()
@open_api.get_list(response_schema=PublisherSchema)
@api.route("/publishers", methods=["GET"])
def publishers_list():
...
@open_api.get_detail(response_schema=PublisherSchema)
@api.route("/publishers/<publisher_id>", methods=["GET"])
def publishers_details(publisher_id):
...
Problem is, this is still not showing correct type in docs:
We need to do some more work for that to happen:
override URL parameter ID definition (like in “Documenting URL parameters”)
override generated type documentation for our new field
First, let’s override URL parameter:
class SchemaOpts(ma.SchemaOpts):
def __init__(self, meta, *args, **kwargs):
self.url_parameters: list[ParameterObject | dict] | None = getattr(
meta, "url_parameters", None
)
super().__init__(meta, *args, **kwargs)
class PublisherSchema(ma.Schema):
OPTIONS_CLASS = SchemaOpts
class Meta:
url_parameters = [
{
"name": "id",
"in": "path",
"required": True,
"allowEmptyValue": False,
"schema": {
k: v
for k, v in UlidField.OPENAPI_SCHEMA_ATTRS.items()
# Not strictly necessary, but reduces spam in docs
if k != "examples"
},
}
]
id = UlidField()
name = ma.fields.String()
address = ma.fields.String()
Finally, we need to inform OpenAPI middleware of our new type:
conf = OpenAPISettings(
api_version="v1", api_name="My API", app_package_name="my_api", mounted_at="/v1"
)
docs = OpenAPI(config=conf)
def Ulid_field2properties(self, field, **kwargs):
if isinstance(field, UlidField):
return UlidField.OPENAPI_SCHEMA_ATTRS.items()
return dict()
docs.add_attribute_function(ULID_field2properties)
docs.init_app(app)
Important
All calls to OpenAPI.add_attribute_function()
must happen before OpenAPI.init_app
.
Obviously, if OpenAPI is initialized like docs = OpenAPI(config, app)
then it is
already to late to call OpenAPI.add_attribute_function()
.
More details on why and how this works are in apispec custom fields documentation
This finally gives us correct type and correct example values in generated docs: