0

I have a function that receives multiple different json string objects with different structure and/or field names, like so:

event = '{"userId": "TDQIQb2fQaORKvCyepDYoZgsoEE3", "profileIsCreated": true}'

or

event = '{"userId": "TDQIQb2fQaORKvCyepDYoZgsoEE3", "signUpFinished": true}'

And I have data classes like so:

from dataclasses import dataclass, field


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass(frozen=True)
class UserId:
    userId: str


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass(frozen=True)
class SignUpFinished(UserId):
    signUpFinished: bool


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass(frozen=True)
class UserProfileCreated(UserId):
    profileIsCreated: bool

Currently, the way I write my function is like this:

def cast_event(event):
    user_details = None

    try:
        user_details = SignUpFinished.from_json(event)
    except KeyError:
        pass

    try:
        user_details = UserProfileCreated.from_json(event)
    except KeyError:
        pass

    if user_details:
        return "OK"
    else:
        return "UNHANDLED"

The problem is, as I have more and more events to handle, my function will become longer and longer, however, it is only doing the same thing.

Is there a better way to achieve what I want to achieve?


I have checked out some of the SO questions:

but they don't seem to be the best way of trying to achieve what I want.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
billydh
  • 975
  • 11
  • 27
  • 1
    I'd advise to try to have a more formal differentiation here, like `{"event": "signupFinished", "data": {...}}`, which then easily allows you to switch on the `event` key to determine what data structure you're dealing with. Brute-forcing it seems like a bad approach, and will get messy if two different dataclasses may be able to deserialise the same JSON. – deceze Jan 16 '20 at 09:02
  • can i confirm what you meant by that? so when sending the `event` to the function `cast_event`, I should add the event type as a field in the payload like you showed? and the actual event will be in `data`? so, i'd imagine i'd have like a lookup `dict` or something to use the proper dataclass? – billydh Jan 16 '20 at 09:46
  • Yes, exactly. Set it up so you can map one value to a specific dataclass, instead of basically needing to look at the entire message as a whole to decide what dataclass it is. – deceze Jan 16 '20 at 09:48
  • Are you able to post an answer with some working code? – billydh Jan 16 '20 at 10:08

3 Answers3

2

Since each case is syntactically the same, you can handle them in a single loop. Iterate through a sequence of cases and try to return; this automatically keeps on trying later cases until one succeeds.

def cast_event(event):
    for case in (UserId , SignUpFinished, UserProfileCreated):
        try:
            return case.from_json(event)
        except KeyError:
            pass
    raise ValueError(f'not a valid event: {event}')
MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
2

While a loop approach works to solve your question as asked, it would be a lot better if you didn't need a "brute force" approach to deserialising your data in the first place. To do that, you'd need a field which unambiguously helped you determine what kind of data structure you're dealing with. E.g.:

event = {'event': 'profile',
         'data': {'userId': 'TDQIQb2fQaORKvCyepDYoZgsoEE3', 'profileIsCreated': True}}

Here the event 'profile' will always be followed by an object with the keys 'userId' and 'profileIsCreated'. That is the guarantee your event messages should make, then it's trivial to parse them:

event_map = {
    'profile': UserProfileCreated,
    ...
}

return event_map[event['event']](**event['data'])

Note that I'm skipping the JSON-parsing step here. You'll need to parse the JSON first to evaluate its event key, so using dataclass_json is probably superfluous/not useful then.

deceze
  • 510,633
  • 85
  • 743
  • 889
  • can you add the part where you parse the json to `dataclass`? i am not sure if i understand your suggestion completely or not. – billydh Jan 16 '20 at 21:07
  • That last line is where the parsed JSON data gets passed to the dataclass constructor. The actual JSON parsing is omitted here, but is a simple call to `json.load` somewhere. – deceze Jan 17 '20 at 05:48
0

For the specified source data, you can do this:

import json

data = '{"userId": "TDQIQb2fQaORKvCyepDYoZgsoEE3", "profileIsCreated": true}'

data = json.loads(data)

user_id = data.pop('userId')
user_details_key = list(data.keys())[0] if data else None
user_details = list(data.values())[0] if data else None

assert user_id == 'TDQIQb2fQaORKvCyepDYoZgsoEE3'
assert user_details_key == 'profileIsCreated'
assert user_details == True
Evgeniy_Burdin
  • 627
  • 5
  • 14