apispec icon indicating copy to clipboard operation
apispec copied to clipboard

marshmallow_enum.EnumField options do not show up in Swagger enum

Open flaker opened this issue 5 years ago • 11 comments

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.

flaker avatar Sep 25 '19 00:09 flaker

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 avatar Sep 25 '19 07:09 lafrech

@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.

Bangertm avatar Sep 25 '19 12:09 Bangertm

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.

lafrech avatar Sep 25 '19 13:09 lafrech

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.

Bangertm avatar Sep 25 '19 13:09 Bangertm

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.

lafrech avatar Sep 25 '19 14:09 lafrech

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.

treidel avatar May 17 '20 19:05 treidel

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
```

lafrech avatar May 17 '20 20:05 lafrech

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.

treidel avatar May 18 '20 00:05 treidel

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 avatar Jul 03 '20 14:07 pierrebai

@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.

killthekitten avatar Sep 07 '20 08:09 killthekitten

Two observations:

  • You can trivially check the field if it takes enum elements by name or by value, by checking theEnumField.by_value attribute.
  • When EnumField.by_value is True, 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 to marshmallow.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.

mjpieters avatar Jun 08 '21 17:06 mjpieters

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.

lafrech avatar Oct 14 '22 22:10 lafrech