apispec
apispec copied to clipboard
marshmallow_enum.EnumField options do not show up in Swagger enum
Hi,
I was using the marshmallow_enum.EnumField
type successfully (validation and enum.Enum
usage). Until I realized that no enum
section is being generated in the swagger description. E.g.:
change_type = EnumField(
ChangeTypes,
required=True,
error='Invalid value specified for "change_type". Valid values are: {names}',
description='Action performed on the entity.'
)
ChangeTypes
is a regular Python3 enum.
The generated swagger looks like:
"change_type": {
"description": "Action performed on the entity."
},
is there a way to get to a fuller Swagger spec while still using Python3 enums? (without going to Str
).
Thanks.
You ought to write a custom field2properties functions.
https://apispec.readthedocs.io/en/latest/using_plugins.html?highlight=add_attribute_function#custom-fields
You may share it here if you do so.
@lafrech were you thinking of including custom attribute functions in apispec? It might be convenient to include them in the same repo as the custom field so that they could potentially change along with that field. That attribute functions shouldn't be dependent on apispec for anything other than testing.
Well, either each custom field repo adds the attribute function and the tests (and the apispec test dependency), assuming the author is willing to (he may not be using apispec at all), either apispec includes those custom functions, at least for a subset of commonly used custom fields from the close and recommended ecosystem, with test dependencies on those fields.
In the latter case, we could even enable the attribute function automatically if the custom field is importable, for even better user experience.
My expectation is that many/all of the custom attribute functions are going to depend on the custom field because the most convenient way to write them is to test if the field is an instance of the custom field at the top of the function - like how the builtin functions for Nested
, List
, and Dict
work.
The other thing is that right now two of those three functions plus the handling of default depend on the marshmallow version because things change over time. If the code for handling that was included with marshmallow everything would just stay in synch. This is manageable for builtin fields, but might get unwieldy - especially for fields that are evolving.
That said it would be cool to auto enable the functions based on importability.
We could add a custom_attribute_functions.py file that would bake a sort of init function that would conditionally (the condition being importability) call add_attribute_function to add the custom field2attributes function.
And on OpenAPIConverter init, this function would be called. It would do nothing if no custom field was importable.
The other fields I can think of are oneofschema, perhaps polyfield, and the hateoas field (#172).
Does not sound elegant, but I think it could work.
Hello All,
I thought I'd revive this thread as I've ran into the same issue. I wrote a simple custom field converter that generically converts marshmallow_enum fields to openapi/swagger enum definitions.
def enum2properties(self, field, **kwargs):
import marshmallow_enum
"""Add an OpenAPI extension for marshmallow_enum.EnumField instances
"""
ret = {}
if isinstance(field, marshmallow_enum.EnumField):
values = []
for member in field.enum:
values.append(member.value)
ret['type'] = 'string'
ret['enum'] = values
return ret
The resulting specification content seems to be the correct syntax according to this reference: https://swagger.io/docs/specification/data-models/enums/
Here's an example YAML fragment of an enum type that is generated:
status:
enum:
- created
- completed
- cancelled
type: string
Apologies for the code - I can't seem to get the code to display properly with the github comments... It seems to me this could be integrated into field_converters.py in the marshmallow plugin. I'll post a pull request if there's interest in integrating this code.
I didn't check the result but thanks for sharing already.
I'd be tempted to use a list comprehension:
if isinstance(field, marshmallow_enum.EnumField):
return {'type': 'string', 'enum': [m.value for m in field.enum]}
return {}
Apologies for the code - I can't seem to get the code to display properly with the github comments...
Use a triple backquote and specify the language.
```py
code
```
Thanks for the hint on code formatting. I tested out your alternative code and it works fine. It's more efficient than mine so I'll post a pull request for it in a little while.
I want to add that instead of m.value, you should use m.name so that the name of enum entry is used instead of its numeric value. Here is the code I'm using:
def enum_to_properties(self, field, **kwargs):
"""
Add an OpenAPI extension for marshmallow_enum.EnumField instances
"""
import marshmallow_enum
if isinstance(field, marshmallow_enum.EnumField):
return {'type': 'string', 'enum': [m.name for m in field.enum]}
return {}
marshmallow_plugin = MarshmallowPlugin()
app.config.update({
'APISPEC_SPEC': APISpec(
title='my-title',
version='v1',
plugins=[marshmallow_plugin],
openapi_version='2.0'
),
'APISPEC_SWAGGER_URL': '/swagger/',
})
marshmallow_plugin.converter.add_attribute_function(enum_to_properties)
@pierrebai keep in mind, not all enum use cases assume an int value. Pretty often m.value
is a string, likely equal to m.name
.
Two observations:
- You can trivially check the field if it takes enum elements by name or by value, by checking the
EnumField.by_value
attribute. - When
EnumField.by_value
isTrue
, the value type could materially differ, it is no longer guaranteed to be a string.EnumField
should really use an inner field definition, defaulting it tomarshmallow.fields.String()
. That's an issue for that project.
All our enums are backed by SQLAlchemy
, which means the values are always strings, so we don't have to worry about my second issue. So, with an assertion the values are strings, I'd use:
def enum2properties(self, field, **kwargs):
if not isinstance(field, EnumField):
return {}
enum_values = [m.value for m in field.enum] if field.by_value else list(field.enum.__members__)
assert all(isinstance(v, str) for v in enum_values), "enum values must be strings"
return {"type": "string", "enum": enum_values}
If you must supporrt by_value=True
for an enum with values other that strings, you will have to do additional work to determine the correct value for the type
field.
I just published apispec 6.0.0 with support for marshmallow.fields.Enum
.
While this doesn't fix this issue, I guess users will be moving to Enum
so this can probably be closed.
Feel free to comment. This can be reopened if needed.