Repository: encode/django-rest-framework

Issue:
Decimal fields rendered as number instead of string in OpenAPI schema
## Checklist

- [x] I have verified that that issue exists against the `master` branch of Django REST framework.
- [x] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
- [x] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
- [x] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.)
- [x] I have reduced the issue to the simplest possible case.
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)

## Steps to reproduce

1. Create a serializer with a decimal field.
2. Use the serializer in a `ViewSet`.
3. Render the OpenAPI schema. 

## Expected behavior
The decimal field should have a type of `string`.

## Actual behavior
The decimal field has a type of `number`.

This issue was introduced by #6674.


Discussion:
Additional context: Besides the schema being wrong—the API returns decimals as strings—JS/TS clients need to represent decimals as strings to avoid losing precision.

Resolving patch:
diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py
--- a/rest_framework/schemas/openapi.py
+++ b/rest_framework/schemas/openapi.py
@@ -15,6 +15,7 @@
 from rest_framework import exceptions, renderers, serializers
 from rest_framework.compat import uritemplate
 from rest_framework.fields import _UnvalidatedField, empty
+from rest_framework.settings import api_settings
 
 from .generators import BaseSchemaGenerator
 from .inspectors import ViewInspector
@@ -325,30 +326,29 @@ def _get_pagination_parameters(self, path, method):
 
     def _map_choicefield(self, field):
         choices = list(OrderedDict.fromkeys(field.choices))  # preserve order and remove duplicates
-        if all(isinstance(choice, bool) for choice in choices):
-            type = 'boolean'
-        elif all(isinstance(choice, int) for choice in choices):
-            type = 'integer'
-        elif all(isinstance(choice, (int, float, Decimal)) for choice in choices):  # `number` includes `integer`
-            # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
-            type = 'number'
-        elif all(isinstance(choice, str) for choice in choices):
-            type = 'string'
-        else:
-            type = None
-
         mapping = {
             # The value of `enum` keyword MUST be an array and SHOULD be unique.
             # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20
             'enum': choices
         }
 
-        # If We figured out `type` then and only then we should set it. It must be a string.
-        # Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type
-        # It is optional but it can not be null.
-        # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
-        if type:
-            mapping['type'] = type
+        if all(isinstance(choice, bool) for choice in choices):
+            mapping['type'] = 'boolean'
+        elif all(isinstance(choice, int) for choice in choices):
+            mapping['type'] = 'integer'
+        elif all(isinstance(choice, Decimal) for choice in choices):
+            mapping['format'] = 'decimal'
+            if api_settings.COERCE_DECIMAL_TO_STRING:
+                mapping['enum'] = [str(choice) for choice in mapping['enum']]
+                mapping['type'] = 'string'
+            else:
+                mapping['type'] = 'number'
+        elif all(isinstance(choice, (int, float, Decimal)) for choice in choices):  # `number` includes `integer`
+            # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
+            mapping['type'] = 'number'
+        elif all(isinstance(choice, str) for choice in choices):
+            mapping['type'] = 'string'
+
         return mapping
 
     def _map_field(self, field):


Test patch:
diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py
--- a/tests/schemas/test_openapi.py
+++ b/tests/schemas/test_openapi.py
@@ -1,5 +1,6 @@
 import uuid
 import warnings
+from decimal import Decimal
 
 import pytest
 from django.conf.urls import url
@@ -78,6 +79,9 @@ def test_list_field_mapping(self):
                 (1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'),
             ])),
                 {'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}),
+            (serializers.ListField(child=
+                                   serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])),
+             {'items': {'enum': ['1.111', '2.222'], 'type': 'string', 'format': 'decimal'}, 'type': 'array'}),
             (serializers.IntegerField(min_value=2147483648),
              {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}),
         ]
@@ -85,6 +89,15 @@ def test_list_field_mapping(self):
             with self.subTest(field=field):
                 assert inspector._map_field(field) == mapping
 
+    @override_settings(REST_FRAMEWORK={'COERCE_DECIMAL_TO_STRING': False})
+    def test_decimal_schema_for_choice_field(self):
+        inspector = AutoSchema()
+        field = serializers.ListField(
+            child=serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')]))
+        mapping = {'items': {'enum': [Decimal('1.111'), Decimal('2.222')], 'type': 'number'}, 'type': 'array'}
+        assert inspector._map_field(field) == mapping
+
+
     def test_lazy_string_field(self):
         class ItemSerializer(serializers.Serializer):
             text = serializers.CharField(help_text=_('lazy string'))