Skip to content

Unknown tag on redered schema when using Django's SafeString as help_text #8428

Closed
@hashlash

Description

@hashlash

Discussed in #8402

Since:

  • there's no response from the discussion thread,
  • I think this is a valid issue, and
  • I've found a possibly proper solution

I'll just put this as an issue and create a PR.

Originally posted by hashlash March 11, 2022

The Issue

I have a model with SafeString as its help_text (inspired from Django's password validation help text):

from django.db import models
from django.utils.html import format_html, format_html_join


def list_help_text_html(help_texts):
    help_items = format_html_join(
        "", "<li>{}</li>", ((help_text,) for help_text in help_texts)
    )
    return format_html("<ul>{}</ul>", help_items) if help_items else ""


class ExampleModel(models.Model):
    field = models.TextField(
        help_text=list_help_text_html([
            'info a',
            'info b',
        ]),
    )

I followed the DRF's doc about Generating a dynamic schema with SchemaView and A minimal example with Swagger UI, but the Swagger UI fails to render the generated schema.

Parser error on line 59
unknown tag !<tag:yaml.org,2002:python/object/new:django.utils.safestring.SafeString>

The generated schema (full version):

openapi: 3.0.2
info:
  # redacted
paths:
  # redacted
components:
  schemas:
    Example:
      type: object
      properties:
        id:
          # redacted
        field:
          type: string
          description: !!python/object/new:django.utils.safestring.SafeString
          - <ul><li>info a</li><li>info b</li></ul>
      required:
      - field

Here's a repo for demonstrating the bug. The Swagger UI can be accessed on /swagger-ui/.

Package versions:

django==3.2.12
djangorestframework==3.13.1
pyyaml==6.0
uritemplate==4.1.1

Findings

I found that the schema generation for the help_text lies on:

if model_field is not None and model_field.help_text:
description = force_str(model_field.help_text)

and:

if field.help_text:
schema['description'] = str(field.help_text)

But since SafeString is a subclass of str and it override the __str__() method to return itself, those DRF's method will still return a SafeString object.

class SafeString(str, SafeData):
    ...

    def __str__(self):
        return self

Workaround

I've with 3 possible workarounds, and so far I think the third is the best:

  • Adding the SafeString with an empty string (implementation, demo)

    >>> from django.utils.safestring import SafeString
    >>> s = SafeString('abc')
    >>> s + ''
    'abc'
    >>> type(s + '')
    <class 'str'>
    
  • Use str.__str__() (implementation, demo)

    >>> from django.utils.safestring import SafeString
    >>> s = SafeString('abc')
    >>> str.__str__(s)
    'abc'
    >>> type(str.__str__(s))
    <class 'str'>
    
  • Use represent_str() to represent SafeString as str (implementation, demo)

    class OpenAPIRenderer(BaseRenderer):
        ...
        def render(self, data, media_type=None, renderer_context=None):
            class Dumper(yaml.Dumper):
                ...
            Dumper.add_representer(SafeString, Dumper.represent_str)
            return yaml.dump(data, default_flow_style=False, sort_keys=False, Dumper=Dumper).encode('utf-8')

PS: I also asked about the "string conversion" on Django's forum.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions