Skip to content

Render JSON fields with proper indentation in browsable API forms. #6243

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

Merged
merged 2 commits into from
Mar 15, 2021

Conversation

aleehedl
Copy link
Contributor

@aleehedl aleehedl commented Oct 15, 2018

Description

Fixes bugs related to nested JSONField as_form_field rendering.

Closes #6211

@aleehedl aleehedl force-pushed the issue-6211-nested-jsonfield branch from 6c568fa to 34b6dce Compare October 15, 2018 06:03
When using json.dumps with indenting, in python2 the default formatting
prints whitespace after commas (,) and python3 does not. This can be
unified with the separators keyword argument.
@aleehedl aleehedl force-pushed the issue-6211-nested-jsonfield branch from 34b6dce to e532cdd Compare October 15, 2018 06:33
@s4ke
Copy link

s4ke commented Mar 12, 2021

Can we get this merged?

@s4ke
Copy link

s4ke commented Mar 12, 2021

Something I had to add to have the proper behaviour in the browsable API was special checking for nulls and errors:

    def as_form_field(self):
        values = {}
        for key, value in self.value.items():
            if isinstance(value, (list, dict)):
                values[key] = value
            else:
                field = self.fields[key]
                error = self.errors.get(key) if isinstance(self.errors, dict) else None
                if getattr(field, '_is_jsonfield', False) and error is not None or value is None:
                    if value is None:
                        values[key] = None
                    else:
                        class JSONString(str):
                            def __new__(cls, value):
                                ret = str.__new__(cls, value)
                                ret.is_json_string = True
                                return ret
                        values[key] = JSONString(value)
                else:
                    values[key] = '' if (value is None or value is False) else force_str(value)
        return self.__class__(self._field, values, self.errors, self._prefix)

@tomchristie tomchristie changed the title Fix JSONBoundField usage on nested serializers (#6211) Render JSON fields with proper indentation in browsable API forms. Mar 15, 2021
@tomchristie
Copy link
Member

Yeah seems like a decent improvement actually. Thanks!

@tomchristie tomchristie merged commit b256c46 into encode:master Mar 15, 2021
@s4ke
Copy link

s4ke commented Mar 15, 2021

@tomchristie My comment was intended as a hint of what we needed to add for proper rendering of nulls and errors for json fields in the browsable api. Do you think that this would help?

@tomchristie
Copy link
Member

Not sure, it's not clear enough to me from the comment what the actual effect/change is that you're suggesting. (From a quick scan)

@s4ke
Copy link

s4ke commented Mar 15, 2021

Right now, the browsable API has the following behaviour for json fields for nested bound fields:

  1. You put in a broken/non-json payload into the JSON field in the browsable api, e.g. {'broken': json}. It quotes the input again to produce "{'broken': json}" in the browsable api on the error page.
  2. You put in a null json value that makes validation fail (json field is not nullable). It quotes the input to "null" on the error page.

While not a big issue, this is a nuissance when testing out the browsable API and you mistyped before sending a request as you have to manually fix the error in your payload as well as the extra quotes around it that were introduced by drf.

EDIT: I will produce screenshots.

@s4ke
Copy link

s4ke commented Mar 15, 2021

Without my snippet:
grafik
grafik

With my snippet:

grafik
grafik

@tomchristie
Copy link
Member

Not sure. We might consider it, tho I don't know if the complexity is worth it or not. We're a big ol' clunky framework as things are, and sometimes it might be worth us just letting a few of these niggles pass, vs. adding extra code workarounds. 🤷‍♂️

Prob needs a fresh discussion if you're keen on pushing for it, and see if others have any feedback. (?)

@s4ke
Copy link

s4ke commented Mar 15, 2021

I don't see it as a big showstopper for us, we have found the workaround and use it in the codepath we need it for. In the end it's your call.

EDIT: And as this is "only" about the browsable API I understand your stance.

If I come around to it I will maybe create a PR. For now, the workaround can be found here.

class PayloadNestedField(NestedBoundField):
    # this is already fixed, but we are at 3.12.1 still
    def __init__(self, field, value, errors, prefix=''):  # type: ignore
        if value is not None and not isinstance(value, Mapping):
            value = {}
        super().__init__(field, value, errors, prefix)

    def __getitem__(self, key: Any) -> Any:
        field = self.fields[key]
        value = self.value.get(key) if self.value else None
        error = self.errors.get(key) if isinstance(self.errors, dict) else None  # type: ignore
        if hasattr(field, 'fields'):
            return NestedBoundField(field, value, error, prefix=self.name + '.') # type: ignore
        if getattr(field, '_is_jsonfield', False):
            return JSONBoundField(field, value, error, prefix=self.name + '.') # type: ignore
        return BoundField(field, value, error, prefix=self.name + '.') # type: ignore

    def as_form_field(self) -> Any:
        values = {}
        for key, value in self.value.items():
            if isinstance(value, (list, dict)):
                values[key] = value
            else:
                field = self.fields[key]
                error = self.errors.get(key) if isinstance(self.errors, dict) else None  # type: ignore
                if getattr(field, '_is_jsonfield', False) and error is not None or value is None:
                    if value is None:
                        values[key] = None
                    else:
                        class JSONString(str):
                            def __new__(cls, value):
                                ret = str.__new__(cls, value)
                                ret.is_json_string = True
                                return ret
                        values[key] = JSONString(value)
                else:
                    values[key] = '' if (value is None or value is False) else force_str(value)  # type: ignore
        return self.__class__(self._field, values, self.errors, self._prefix)  # type: ignore

stefanacin pushed a commit to stefanacin/django-rest-framework that referenced this pull request Mar 22, 2021
…ncode#6243)

* Fix JSONBoundField usage on nested serializers (encode#6211)

* Unify JSONBoundField as_form_field output between py2 and py3

When using json.dumps with indenting, in python2 the default formatting
prints whitespace after commas (,) and python3 does not. This can be
unified with the separators keyword argument.
@tomchristie tomchristie mentioned this pull request Mar 25, 2021
sigvef pushed a commit to sigvef/django-rest-framework that referenced this pull request Dec 3, 2022
…ncode#6243)

* Fix JSONBoundField usage on nested serializers (encode#6211)

* Unify JSONBoundField as_form_field output between py2 and py3

When using json.dumps with indenting, in python2 the default formatting
prints whitespace after commas (,) and python3 does not. This can be
unified with the separators keyword argument.
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 this pull request may close these issues.

JSONField form data in a related model renders as python dict
3 participants