-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Give a more informative error for JSON not serializable. (#269) #273
Conversation
Very nice! One case that I'm interested in is how we can deal with objects that have a non-JSON-serializable object deep inside an object, for example:
With this current exception, it would say that "Div is not JSON serializable" which could be misleading. In an ideal case, we would be able to say something like:
Or, if the particular parent object was assigned an ID, like
then our error message could be like:
Now, I'm not actually sure the best way to go about doing this. One way might be to subclass the It's been a few years since I worked with that class, but I believe that So perhaps we could raise a nicer exception within there somehow. I would be OK with our own JSON encoder class that uses that Alternatively, we might be able to traverse the tree ourselves. If the object isn't serializable, then we could walk down the component tree until we find the particular property that isn't serializable. I actually wrote a method that traverses the tree: dash/dash/development/base_component.py Line 152 in f309cc2
which I believe you can call with something like:
However, I don't think that really gives us the path of the component, it would only give us each individual component in the tree. Another alternative would be to just raise an exception for the particular property and print the entire component. The user might be able to figure out which component was having the issue if they say both the property that wasn't JSON serializable and if the saw the rest of the properties that weren't serializable. For example, if the issue was something like:
Then the error message would be like:
I believe this would be relatively easy with the Oddly, I can't seem to find any other solution out there. |
I think a good solution would be to edit
then
We can then test if every value in this traversal is one of these types actually, we would need to do something like
to account for that python 2 edge case. This can give very detailed exceptions, which are constructed here: https://github.com/rmarren1/dash/blob/da65b56ebd532ebf9081424467bf100d3a537f44/dash/dash.py#L481 One example I tried was returning this from a callback:
which raises this
|
😻 that looks SO good @rmarren1 ! This seems like the winning solution. @plotly/dash - Anyone else like to review? |
Love this! I would recommend putting each path element on its own line rather than separating them with We could also structure the lines a bit differently like:
or something? Maybe add in the |
It turns out that
since the My quick patch was to check each value in the traversal, and if is a Also, I changed the output format (thanks @nicolaskruchten). Outputs now read
I also added special cases for when the bad value is the only value returned or is a list (previously, the error message would talk about trees and print out a tree where there is only one element, this could be confusing). These error messages look like this now:
|
Nice! I would favour the |
I think that was just a coincidence in the example I used, which was something like this:
If the When an element is a singleton child the prefix is '- ' so that it aligns with the list elements that have prefix '[i] ' I will double check this |
Right, so what I'm saying is that even if elements are singletons, I think we should show |
How could we then differentiate between a singleton element and the first element in a list?
would need to be accessed like this:
and
would need to be accessed like this:
Showing I think that would make sense if Dash automatically converted singleton elements into length one lists (which might make coding a bunch of the internals easier). |
Ah, I see what the problem is. In that case I might suggest |
Error paths now read like this:
looks better imo, thanks @nicolaskruchten |
Is this good to merge? |
@@ -475,6 +475,110 @@ def _validate_callback(self, output, inputs, state, events): | |||
output.component_id, | |||
output.component_property).replace(' ', '')) | |||
|
|||
def _validate_callback_output(self, output_value, output): | |||
valid = [str, dict, int, float, type(None), Component] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we JSON serialize with the json.dumps(obj, cls=plotly.utils.PlotlyJSONEncoder)
, this list actually has a few other items: https://github.com/plotly/plotly.py/blob/6b3a0135977b92b3f7e0be2a1e8b418d995c70e3/plotly/utils.py#L137-L332
So, if there was a component property that accepted a list of numbers, then technically the user could return a numpy array or a dataframe.
The only example that immediately comes to mind is updating the figure
property in a callback with the plotly.graph_objs
. These objects get serialized because they have a to_plotly_json
method.
So, I wonder if instead of validating up-front, we should only run this routine only if our json.dumps(resp, plotly.utils.PlotlyJSONEncoder)
call fails.
This would have the other advantage of being faster for the non-error case. If the user returns a huge object (e.g. some of my callbacks in apps return a 2MB nested component), doing this recursive validation might be slow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense. I am not sure how large of a speed up that would be since validating a large callback output is likely much faster than the network delay of sending that large output, but it definitely makes it easier than crafting a perfect validator (that would need to update every time we want to extend PlotlyJSONEncoder
).
I have just one more comment about the placement of the validation call. Otherwise, I'm 👍 with the code. It would be great if another dash dev could review this so that more people are familiar with this code. @T4rk1n , could you take a look as well? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall.
dash/exceptions.py
Outdated
@@ -44,3 +44,7 @@ class CantHaveMultipleOutputs(CallbackException): | |||
|
|||
class PreventUpdate(CallbackException): | |||
pass | |||
|
|||
|
|||
class ReturnValueNotJSONSerializable(CallbackException): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just me, but I don't like that exception name, it gives too much info on the cause while not giving the true nature of the error, that is the return value of a callback is invalid. I would change it to InvalidCallbackReturnValue
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to me, pushed those changes.
response, | ||
cls=plotly.utils.PlotlyJSONEncoder | ||
) | ||
except TypeError: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 perfect
💃 Looks good, let's do this! |
Update changelog and version for #273
Fix for #269
when a non JSON serializable object is returned from a dash callback, the following exception is raised:
an example app:
results in:
when the button is clicked (and returns the dash module rather than a value).