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
@@ -442,11 +443,17 @@ def _map_field(self, field):
                 content['format'] = field.protocol
             return content
 
-        # DecimalField has multipleOf based on decimal_places
         if isinstance(field, serializers.DecimalField):
-            content = {
-                'type': 'number'
-            }
+            if getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING):
+                content = {
+                    'type': 'string',
+                    'format': 'decimal',
+                }
+            else:
+                content = {
+                    'type': 'number'
+                }
+
             if field.decimal_places:
                 content['multipleOf'] = float('.' + (field.decimal_places - 1) * '0' + '1')
             if field.max_whole_digits:
@@ -457,7 +464,7 @@ def _map_field(self, field):
 
         if isinstance(field, serializers.FloatField):
             content = {
-                'type': 'number'
+                'type': 'number',
             }
             self._map_min_max(field, content)
             return content
@@ -556,7 +563,8 @@ def _map_field_validators(self, field, schema):
                 schema['maximum'] = v.limit_value
             elif isinstance(v, MinValueValidator):
                 schema['minimum'] = v.limit_value
-            elif isinstance(v, DecimalValidator):
+            elif isinstance(v, DecimalValidator) and \
+                    not getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING):
                 if v.decimal_places:
                     schema['multipleOf'] = float('.' + (v.decimal_places - 1) * '0' + '1')
                 if v.max_digits:


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
@@ -838,6 +838,16 @@ def test_serializer_validators(self):
         assert properties['decimal2']['type'] == 'number'
         assert properties['decimal2']['multipleOf'] == .0001
 
+        assert properties['decimal3'] == {
+            'type': 'string', 'format': 'decimal', 'maximum': 1000000, 'minimum': -1000000, 'multipleOf': 0.01
+        }
+        assert properties['decimal4'] == {
+            'type': 'string', 'format': 'decimal', 'maximum': 1000000, 'minimum': -1000000, 'multipleOf': 0.01
+        }
+        assert properties['decimal5'] == {
+            'type': 'string', 'format': 'decimal', 'maximum': 10000, 'minimum': -10000, 'multipleOf': 0.01
+        }
+
         assert properties['email']['type'] == 'string'
         assert properties['email']['format'] == 'email'
         assert properties['email']['default'] == 'foo@bar.com'
diff --git a/tests/schemas/views.py b/tests/schemas/views.py
--- a/tests/schemas/views.py
+++ b/tests/schemas/views.py
@@ -119,9 +119,13 @@ class ExampleValidatedSerializer(serializers.Serializer):
             MinLengthValidator(limit_value=2),
         )
     )
-    decimal1 = serializers.DecimalField(max_digits=6, decimal_places=2)
-    decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0,
+    decimal1 = serializers.DecimalField(max_digits=6, decimal_places=2, coerce_to_string=False)
+    decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0, coerce_to_string=False,
                                         validators=(DecimalValidator(max_digits=17, decimal_places=4),))
+    decimal3 = serializers.DecimalField(max_digits=8, decimal_places=2, coerce_to_string=True)
+    decimal4 = serializers.DecimalField(max_digits=8, decimal_places=2, coerce_to_string=True,
+                                        validators=(DecimalValidator(max_digits=17, decimal_places=4),))
+    decimal5 = serializers.DecimalField(max_digits=6, decimal_places=2)
     email = serializers.EmailField(default='foo@bar.com')
     url = serializers.URLField(default='http://www.example.com', allow_null=True)
     uuid = serializers.UUIDField()