Skip to content

enum.IntEnum is not compatible with JSON serialisation #62464

Closed

Description

@ncoghlan
BPO 18264
Nosy @gvanrossum, @warsaw, @rhettinger, @amauryfa, @ncoghlan, @pitrou, @giampaolo, @ezio-melotti, @ethanfurman, @ericsnowcurrently
Files
  • issue18264.1-prelim.patch
  • issue18264.stoneleaf.01.patch
  • issue18264.stoneleaf.02.patch
  • issue18264.stoneleaf.03.patch
  • issue18264.stoneleaf.04.patch
  • issue18264.stoneleaf.05.patch
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/ethanfurman'
    closed_at = <Date 2013-08-10.20:01:54.289>
    created_at = <Date 2013-06-19.13:45:41.046>
    labels = ['type-feature', 'library']
    title = 'enum.IntEnum is not compatible with JSON serialisation'
    updated_at = <Date 2013-08-10.20:01:54.287>
    user = 'https://github.com/ncoghlan'

    bugs.python.org fields:

    activity = <Date 2013-08-10.20:01:54.287>
    actor = 'python-dev'
    assignee = 'ethan.furman'
    closed = True
    closed_date = <Date 2013-08-10.20:01:54.289>
    closer = 'python-dev'
    components = ['Library (Lib)']
    creation = <Date 2013-06-19.13:45:41.046>
    creator = 'ncoghlan'
    dependencies = []
    files = ['31134', '31141', '31155', '31156', '31172', '31173']
    hgrepos = []
    issue_num = 18264
    keywords = ['patch']
    message_count = 50.0
    messages = ['191467', '191470', '191471', '191473', '191474', '191475', '191476', '191477', '191478', '191479', '191480', '191481', '191491', '191498', '191586', '191587', '191588', '191601', '191625', '191626', '191628', '191630', '191631', '191632', '191633', '191634', '191635', '194219', '194220', '194222', '194223', '194266', '194280', '194283', '194289', '194306', '194322', '194327', '194330', '194360', '194401', '194403', '194536', '194539', '194541', '194543', '194554', '194556', '194558', '194828']
    nosy_count = 13.0
    nosy_names = ['gvanrossum', 'barry', 'rhettinger', 'amaury.forgeotdarc', 'ncoghlan', 'pitrou', 'giampaolo.rodola', 'ezio.melotti', 'eli.bendersky', 'cvrebert', 'ethan.furman', 'python-dev', 'eric.snow']
    pr_nums = []
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue18264'
    versions = ['Python 3.4']

    Activity

    1. ncoghlan commented on Jun 19, 2013

      @ncoghlan
      ContributorAuthor

      Replacing an integer constant with the current incarnation of enum.IntEnum breaks JSON serialisation:

      >>> from enum import Enum
      >>> from enum import Enum, IntEnum
      >>> class Example(IntEnum):
      ...     x = 1
      ... 
      >>> import json
      >>> json.dumps(1)
      '1'
      >>> json.loads(json.dumps(1))
      1
      >>> json.dumps(Example.x)
      'Example.x'
      >>> json.loads(json.dumps(Example.x))
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "/home/ncoghlan/devel/py3k/Lib/json/__init__.py", line 316, in loads
          return _default_decoder.decode(s)
        File "/home/ncoghlan/devel/py3k/Lib/json/decoder.py", line 344, in decode
          obj, end = self.raw_decode(s, idx=_w(s, 0).end())
        File "/home/ncoghlan/devel/py3k/Lib/json/decoder.py", line 362, in raw_decode
          raise ValueError(errmsg("Expecting value", s, err.value)) from None
      ValueError: Expecting value: line 1 column 1 (char 0)

      It breaks for floats as well, but in a slightly different way:

      >>> class FloatExample(float, Enum):
      ...     x = 1.0
      ... 
      >>> json.dumps(FloatExample.x)
      '<FloatExample.x: 1.0>'

      Allowing __str__ to be inherited from Enum rather than from the concrete type will similarly break any serialisation protocol that relies on str() to handle integers. The float case is even trickier, since failing to inherit __repr__ would rather miss the point of the whole exercise...

      We're going to have to be *very* careful with swapping out existing constants with enums, and we should be warning about the pitfalls in the docs too (where we can't change enum to avoid them).

    2. added
      stdlibStandard Library Python modules in the Lib/ directory
      on Jun 19, 2013
    3. warsaw commented on Jun 19, 2013

      @warsaw
      Member

      On Jun 19, 2013, at 01:45 PM, Nick Coghlan wrote:

      Allowing __str__ to be inherited from Enum rather than from the concrete type
      will similarly break any serialisation protocol that relies on str() to
      handle integers. The float case is even trickier, since failing to inherit
      repr would rather miss the point of the whole exercise...

      Serialization isn't the only issue - you have to know how to deserialize as
      well. I use JSON extensively in Mailman's REST API. For enums I always
      encode them to just the member name, because I always know what enum class
      will be used to deserialize them.

      (TBH, I wish the json module had better hooks for extending both serialization
      and deserialization of non-basic types.)

    4. ericsnowcurrently commented on Jun 19, 2013

      @ericsnowcurrently
      Member

      It's for times like this that I wonder if a simple serialization protocol might be worth it--something separate from __str__() but much simpler than the pickle protocol. __str__() could be the fallback. In the end, though, I always conclude that it's not worth adding yet another optional protocol to objects. Still, it would help in this case (at least for anything that knows to make use of the simple serialization protocol).

      Alternately, would it make sense to add special handling of enums to the json module? Do we already with pickle?

    5. amauryfa commented on Jun 19, 2013

      @amauryfa
      Contributor

      in encoder.py:
      ...
      elif instance(value, int):
      yield buf + str(value)
      ...
      What if we use int(str(value)) instead?

    6. ericsnowcurrently commented on Jun 19, 2013

      @ericsnowcurrently
      Member

      Serialization isn't the only issue - you have to know how
      to deserialize as well.

      +1

      (TBH, I wish the json module had better hooks for extending
      both serialization and deserialization of non-basic types.)

      +1

      There's at least one stdlib module that does this pretty well (sqlite3, I think). This is where a simple serialization (and deserialization of course) protocol would come in handy.

    7. ethanfurman commented on Jun 19, 2013

      @ethanfurman
      Member

      Nick, in your example it looks like json is using the __str__ for int, but the __repr__ for float -- is this correct?

    8. self-assigned this
      on Jun 19, 2013
    9. ethanfurman commented on Jun 19, 2013

      @ethanfurman
      Member

      Eric,

      The pickle support is solely in Enum. No changes were made to pickles.

    10. pitrou commented on Jun 19, 2013

      @pitrou
      Member

      What if we use int(str(value)) instead?

      You mean str(int(value)). Sounds good to me.

    11. pitrou commented on Jun 19, 2013

      @pitrou
      Member

      Serialization isn't the only issue - you have to know how to
      deserialize as well.

      I think this is pretty much besides the point. IntEnums are meant to be substitutible with plain ints, so you can deserialize as a plain int.

      Moreoven, JSON doesn't fill the same use cases as pickle: it is meant to communicate with services written in any languages, and those services won't have the same enum mnemonics as your Python library.

    12. warsaw commented on Jun 19, 2013

      @warsaw
      Member

      On Jun 19, 2013, at 02:09 PM, Eric Snow wrote:

      There's at least one stdlib module that does this pretty well (sqlite3, I
      think). This is where a simple serialization (and deserialization of course)
      protocol would come in handy.

      Yeah, my database layer also has to be taught how to handle enums!

    13. ncoghlan commented on Jun 19, 2013

      @ncoghlan
      ContributorAuthor

      While I agree with forcing int subclasses to true integers in the JSON module, that may not be enough - the problem will affect third party serialisers as well. Whiel the debugging gains won't be as high, we may need to override __str__() in enum.IntEnum to give the "raw" form (leaving the debugging form solely for repr).

      As for the float behaviour, the JSON serialiser is almost certainly playing games to avoid the truncation that used to occur in float.__str__ (I don't recall if we actually got rid of that, or if it's just a lot harder to trigger these days - it definitely changed when float.__repr__ was updated to prefer human friendly representations that produce the same floating point number). I'm less concerned about that, since we don't plan to swap out any standard library floats for enums. We may still want to mention the potential compatibility issue somewhere in the docs, though.

      Backwards compatibility is such fun, isn't it? :)

    14. ethanfurman commented on Jun 19, 2013

      @ethanfurman
      Member

      I have no problem with leaving __str__ as the inherited type's, and just keeping the __repr__ as the enum add-on; this could be one of the differences between a pure Enum and a hybrid Enum.

      Is it safe to change the json behaviour back to using __str__ for floats? If so, those two updates and we could be good to go. (Famous last words? ;)

    15. ethanfurman commented on Jun 19, 2013

      @ethanfurman
      Member

      I was unable to find any references to previous problems with json and floats. A quick-n-dirty patch yields the following:

      --> from json import dumps, loads
      --> from enum import Enum
      --> class FE(float, Enum):
      ... pass
      ...
      --> class Test(FE):
      ... one = 1.0
      ...
      --> Test.one
      <Test.one: 1.0>
      --> str(Test.one)
      '1.0'
      --> dumps(Test.one)
      '1.0'
      --> loads(dumps(Test.one))
      1.0

      All json and enum tests are still passing.

      If this is an acceptable solution I'll create a nicer patch and post for review.

    16. 25 remaining items

    17. ncoghlan commented on Aug 4, 2013

      @ncoghlan
      ContributorAuthor

      The two "isinstance" checks that bother me are the ones for int and float. However, given that the JSON serialiser *already* includes those explicit checks, I now think it makes sense to just do the minimal fix of coercing subclasses to the base type for both of those cases, especially since such a change should be able to be ported to simplejson as well.

      Anything more elaborate (including making JSON explicitly enum aware, or else offering a new encoder that supports singledispatch) should be handled in a separate issue.

    18. elibendersky commented on Aug 4, 2013

      elibenderskymannequin
      Mannequin

      I also think that exchanging the explicit type checks by __index__ merits more thought and is outside the scope of this local fix. The proposed patch does not add any new type checks, but acts within the bounds of code for which the type is already established.

    19. ethanfurman commented on Aug 4, 2013

      @ethanfurman
      Member

      This patch handles both float and int subclasses, and includes fixes/improvements from the review.

    20. ethanfurman commented on Aug 4, 2013

      @ethanfurman
      Member

      Forgot to add float tests. Included in this patch.

    21. ethanfurman commented on Aug 6, 2013

      @ethanfurman
      Member

      Hopefully the final patch. :)

    22. ethanfurman commented on Aug 6, 2013

      @ethanfurman
      Member

      Forgot to back out core dump tests before creating previous patch. This one even passes the tests. :/

    23. elibendersky commented on Aug 6, 2013

      elibenderskymannequin
      Mannequin

      LGTM now. Make sure to run the test(s) in refleak mode and let's wait for a couple of days before committing, in case someone else has comments.

    24. ethanfurman commented on Aug 6, 2013

      @ethanfurman
      Member

      I'll plan on committing no sooner than Friday unless somebody else has comments/corrections.

      Thanks, Eli.

    25. ethanfurman commented on Aug 6, 2013

      @ethanfurman
      Member

      Eli Bendersky added the comment:

      Make sure to run the test(s) in refleak mode . . .

      How extensive should testing be?

      I plan on always running the refleak mode tests (now that I know how ;) .

      Should I also run non-refleak tests?

      Should I run tests with a python built without --with-pydebug?

    26. elibendersky commented on Aug 6, 2013

      elibenderskymannequin
      Mannequin

      IMHO it's very much dependent on the change. When making C code changes, I usually run the relevant tests with refleaks, and then run the whole test suite; all of this --with-pydebug. Personally I've not encountered cases where non-debug builds failed where debug suceeded for CPython, but for very significant changes I may do some extra runs in the release mode as well (or try to test it on a specific OS). It's very important to watch the bots after committing, because they run a wide variety of configurations you'll be hard pressed to reproduce on your own - so they exercise the change pretty well.

    27. ezio-melotti commented on Aug 6, 2013

      @ezio-melotti
      Member

      I plan on always running the refleak mode tests (now that I know how ;)

      FWIW make patchcheck should remind you about that.

    28. python-dev commented on Aug 10, 2013

      python-devmannequin
      Mannequin

      New changeset ae1a7c420f08 by Ethan Furman in branch 'default':
      Close bpo-18264: int- and float-derived enums now converted to int or float.
      http://hg.python.org/cpython/rev/ae1a7c420f08

    29. transferred this issue fromon Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions