Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum by_value creates OpenAPI property without type #875

Closed
zedrdave opened this issue Jan 16, 2024 · 11 comments · Fixed by #876
Closed

Enum by_value creates OpenAPI property without type #875

zedrdave opened this issue Jan 16, 2024 · 11 comments · Fixed by #876

Comments

@zedrdave
Copy link

If using a native Enum:

import enum
class FoobarEnum(enum.Enum):
    FOO = "foo"
    BAR = "bar"

then using it in a schema:

foobar = fields.Enum(FoobarEnum, by_value=False)

We get the correct OpenAPI JSON:

                  "foobar": {
                    "type": "string",
                    "enum": [
                      "FOO",
                      "BAR"
                    ]
                  },

But if we set by_value to True, we get:

                  "foobar": {
                    "enum": [
                      "foo",
                      "bar"
                    ]
                  },

The missing "type": "string", trips up Swagger, which does not display the enum contents…

This seems potentially related to #807, but I can't see anything in the code that would lead to this difference (I can't quite figure where type is inferred)…

@zedrdave
Copy link
Author

@sloria if you are able to point me in the direction of the code that defines how that type is decided, I would be happy to dig further and hopefully submit a PR

@sloria
Copy link
Member

sloria commented Jan 17, 2024

@zedrdave thanks for volunteering!

i believe the relevant method is field2type_and_format, which takes a marshmallow field and returns the corresponding OAI type and format as a dict.

relevant test is here:

def test_enum_value_field(spec_fixture, by_value):

@Bangertm
Copy link
Collaborator

A couple more details on how the type is decided:

For enum fields there is enum2properties. This is indirectly calling field2type_and_format with the enum field's field.field property. In the case where by_value is True, field.field is a marshmallow.fields.Field instance. This class is set to have no type property in the field mapping:

marshmallow.fields.Field: (None, None),

That's typically because apispec can't infer the type of a generic field.

Not sure what the proper behavior would be in this case because I don't have any experience working with enum fields.

@zedrdave
Copy link
Author

@Bangertm thanks for diving into this further. I must admit I'll need a bit of time to potentially wrap my head around that field.field thing… I don't quite understand why it would be Field in the by_value = True case, and not when False

But basically the behaviour we want to achieve is: type = "string" no matter what, if the Field is enum
It seems like adding a simple ret["type"] = "string" on line 515 would fix it, but I suspect this is not the cleanest way to do it…

@zedrdave
Copy link
Author

zedrdave commented Jan 17, 2024

@Bangertm OK, I think I understand better, and as best as I can tell, this is an upstream issue with Marshmallow setting field to Field() in the by_value case instead of string otherwise (not sure I understand the reasoning): https://marshmallow.readthedocs.io/en/stable/_modules/marshmallow/fields.html#Enum

Short of this being changed upstream, it seems like the only option would be to correct it inside enum2properties as I suggested above, maybe around line 518…?

@zedrdave
Copy link
Author

FWIW: passing by_value=fields.String instead of by_value=True does fix the problem…
But I am not sure it makes sense ever omitting the string type for Enums in OpenAPI, so maybe checking for by_value is True and setting it if missing, would be a good way to solve this?

@lafrech
Copy link
Member

lafrech commented Jan 17, 2024

I don't remember the whole story, there may be retro-compatibility involved, but I think the rationale in marshmallow is that when using by_value=True, without any more information about the value type, marshmallow can only use Field and hope the value doesn't need any serialization specifics. Any non-trivial type must pass a field class. In fact, even strings "should" probably pass a field type. Should marshmallow set String by default? Not sure.

In any case, my advice would be to set by_value=ma.fields.String when needed.

I wouldn't expect apispec to set a string type if nothing is specified. If values are integers, for instance, documenting them as string would be wrong.

@zedrdave
Copy link
Author

zedrdave commented Jan 17, 2024

Indeed, by_value=ma.fields.String does fix it.
It seems a little weird to me that this is needed, when Marshmallow could, at the very least, infer the enum type and use that (or just assume str unless a Field is specified), but that is indeed more of a Marshmallow usability issue than apispec.

I'm not sure I see how adding a default type (if no Field is specified) would hurt:

  • in the vast majority of cases (regular string enum), it would prevent people bumping into the same weird issue.
  • in cases where people explicitly want a numeric enum, they can specify Field

@lafrech
Copy link
Member

lafrech commented Jan 17, 2024

I understand. But I still think explicit is better than implicit. Also, I'm afraid using String rather than Field if nothing is specified would be a breaking change.

More history here if you're interested (I'm the author of the PRs adding Enum to marshmallow):
marshmallow-code/marshmallow#2017
marshmallow-code/marshmallow#2044

We use Field by default no deserialization is needed since Enum validates the content anyway so we don't really care. And we figured people wouldn't bother setting a field for this reason, but it appears this doesn't work well for the apispec case (hence this issue).

@zedrdave
Copy link
Author

zedrdave commented Jan 18, 2024

I would argue there is already a strong implicit behaviour (people setting by_value to True and assuming everything will work), but you definitely have a better view on the overall problem, so I'll defer to you.

I would still recommend finding a way to warn users of that behaviour (if only by putting a note in marshmallow's Enum doc recommending to set by_value to an explicit type if possible).

Happy to close this in any case.

@sloria
Copy link
Member

sloria commented Jan 18, 2024

Upon reading this thread more closely, I agree that explicitly passing a field to by_value is the best approach. I've added a specific note about this in the docs in #876 .

Closing for now. Thanks for the discussion all!

@sloria sloria closed this as completed Jan 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants