def resolve_serializer(self, serializer, direction) -> ResolvedComponent: assert is_serializer(serializer) serializer = force_instance(serializer) component = ResolvedComponent( name=self._get_serializer_name(serializer, direction), type=ResolvedComponent.SCHEMA, object=serializer, ) if component in self.registry: return self.registry[component] # return component with schema self.registry.register(component) component.schema = self._map_serializer(serializer, direction) # 4 cases: # 1. polymorphic container component -> use # 2. concrete component with properties -> use # 3. concrete component without properties -> prob. transactional so discard # 4. explicit list component -> demultiplexed at usage location so discard keep_component = ( any(nest_tag in component.schema for nest_tag in ['oneOf', 'allOf', 'anyOf']) or component.schema.get('properties', {}) ) if not keep_component: del self.registry[component] return ResolvedComponent(None, None) # sentinel return component
def _get_response_for_code(self, serializer): serializer = force_instance(serializer) if not serializer: return {'description': _('No response body')} elif isinstance(serializer, serializers.ListSerializer): if is_serializer(serializer.child): schema = self.resolve_serializer(serializer.child, 'response').ref else: schema = self._map_serializer_field(serializer.child, 'response') elif is_serializer(serializer): component = self.resolve_serializer(serializer, 'response') if not component.schema: return {'description': _('No response body')} schema = component.ref elif is_basic_type(serializer): schema = build_basic_type(serializer) elif isinstance(serializer, dict): # bypass processing and use given schema directly schema = serializer else: warn( f'could not resolve "{serializer}" for {self.method} {self.path}. Expected either ' f'a serializer or some supported override mechanism. defaulting to ' f'generic free-form object.') schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = _('Unspecified response body') if self._is_list_view(serializer) and not get_override( serializer, 'many') is False: schema = build_array_type(schema) paginator = self._get_paginator() if paginator and is_serializer(serializer): paginated_name = f'Paginated{self._get_serializer_name(serializer, "response")}List' component = ResolvedComponent( name=paginated_name, type=ResolvedComponent.SCHEMA, schema=paginator.get_paginated_response_schema(schema), object=paginated_name, ) self.registry.register(component) schema = component.ref elif paginator: schema = paginator.get_paginated_response_schema(schema) return { 'content': { mt: { 'schema': schema } for mt in self.map_renderers('media_type') }, # Description is required by spec, but descriptions for each response code don't really # fit into our model. Description is therefore put into the higher level slots. # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject 'description': '' }
def resolve_serializer(self, serializer, direction) -> ResolvedComponent: assert is_serializer(serializer), ( f'internal assumption violated because we expected a serializer here and instead ' f'got a "{serializer}". This may be the result of another app doing some unexpected ' f'magic or an invalid internal call. Feel free to report this as a bug at ' f'https://github.com/tfranzel/drf-spectacular/issues ') serializer = force_instance(serializer) component = ResolvedComponent( name=self._get_serializer_name(serializer, direction), type=ResolvedComponent.SCHEMA, object=serializer, ) if component in self.registry: return self.registry[component] # return component with schema self.registry.register(component) component.schema = self._map_serializer(serializer, direction) # 4 cases: # 1. polymorphic container component -> use # 2. concrete component with properties -> use # 3. concrete component without properties -> prob. transactional so discard # 4. explicit list component -> demultiplexed at usage location so discard keep_component = (any(nest_tag in component.schema for nest_tag in ['oneOf', 'allOf', 'anyOf']) or component.schema.get('properties', {})) if not keep_component: del self.registry[component] return ResolvedComponent(None, None) # sentinel return component
def _map_basic_serializer(self, serializer, direction): serializer = force_instance(serializer) required = set() properties = {} for field in serializer.fields.values(): if isinstance(field, serializers.HiddenField): continue schema = self._map_serializer_field(field, direction) # skip field if there is no schema for the direction if not schema: continue if field.required or schema.get('readOnly'): required.add(field.field_name) self._map_field_validators(field, schema) properties[field.field_name] = safe_ref(schema) if spectacular_settings.COMPONENT_SPLIT_PATCH: if self.method == 'PATCH' and direction == 'request': required = [] return build_object_type( properties=properties, required=required, description=inspect.getdoc(serializer), )
def map_serializer(self, auto_schema, method: str): """ custom handling for @extend_schema's injection of PolymorphicProxySerializer """ serializer = self.target sub_components = [] for sub_serializer in serializer.serializers: sub_serializer = force_instance(sub_serializer) resolved_sub_serializer = auto_schema.resolve_serializer( method, sub_serializer) try: discriminator_field = sub_serializer.fields[ serializer.resource_type_field_name] resource_type = discriminator_field.to_representation(None) except: # noqa: E722 warn( f'sub-serializer {resolved_sub_serializer.name} of {serializer.component_name} ' f'must contain the discriminator field "{serializer.resource_type_field_name}". ' f'defaulting to sub-serializer name, but schema will likely not match the API.' ) resource_type = resolved_sub_serializer.name sub_components.append((resource_type, resolved_sub_serializer.ref)) return { 'oneOf': [ref for _, ref in sub_components], 'discriminator': { 'propertyName': serializer.resource_type_field_name, 'mapping': { resource_type: ref['$ref'] for resource_type, ref in sub_components } } }
def _get_request_body(self): # only unsafe methods can have a body if self.method not in ('PUT', 'PATCH', 'POST'): return None serializer = force_instance(self.get_request_serializer()) request_body_required = False if is_list_serializer(serializer): if is_serializer(serializer.child): component = self.resolve_serializer(serializer.child, 'request') schema = build_array_type(component.ref) else: schema = build_array_type( self._map_serializer_field(serializer.child, 'request')) request_body_required = True elif is_serializer(serializer): if self.method == 'PATCH': serializer.partial = True component = self.resolve_serializer(serializer, 'request') if not component.schema: # serializer is empty so skip content enumeration return None schema = component.ref # request body is only required if any required property is not read-only readonly_props = [ p for p, s in component.schema.get('properties', {}).items() if s.get('readOnly') ] required_props = component.schema.get('required', []) request_body_required = any(req not in readonly_props for req in required_props) elif is_basic_type(serializer): schema = build_basic_type(serializer) if not schema: return None else: warn( f'could not resolve request body for {self.method} {self.path}. defaulting to generic ' 'free-form object. (maybe annotate a Serializer class?)') schema = build_object_type( additionalProperties={}, description='Unspecified request body', ) request_body = { 'content': { media_type: build_media_type_object( schema, self._get_examples(serializer, 'request', media_type)) for media_type in self.map_parsers() } } if request_body_required: request_body['required'] = request_body_required return request_body
def _map_serializer(self, method, serializer): serializer = force_instance(serializer) serializer_extension = OpenApiSerializerExtension.get_match(serializer) if serializer_extension: return serializer_extension.map_serializer(self, method) else: return self._map_basic_serializer(method, serializer)
def _get_explicit_sub_components(self, auto_schema, direction): sub_components = [] for resource_type, sub_serializer in self.target.serializers.items(): sub_serializer = force_instance(sub_serializer) resolved_sub_serializer = auto_schema.resolve_serializer( sub_serializer, direction) sub_components.append((resource_type, resolved_sub_serializer.ref)) return sub_components
def _map_serializer(self, serializer, direction): serializer = force_instance(serializer) serializer_extension = OpenApiSerializerExtension.get_match(serializer) if serializer_extension: schema = serializer_extension.map_serializer(self, direction) else: schema = self._map_basic_serializer(serializer, direction) return self._postprocess_serializer_schema(schema, serializer, direction)
def map_parsers(self): """ Get request parsers. Handling cases with `FileField`. """ parsers = super().map_parsers() serializer = force_instance(self.get_request_serializer()) for field_name, field in getattr(serializer, "fields", {}).items(): if isinstance(field, serializers.FileField) and self.method in ("PUT", "PATCH", "POST"): return ["multipart/form-data", "application/x-www-form-urlencoded"] return parsers
def _map_response_type_hint(self, method): hint = get_override(method, 'field') or typing.get_type_hints(method).get('return') if is_serializer(hint) or is_field(hint): return self._map_serializer_field(force_instance(hint), 'response') try: return resolve_type_hint(hint) except UnableToProceedError: warn( f'unable to resolve type hint for function "{method.__name__}". consider ' f'using a type hint or @extend_schema_field. defaulting to string.' ) return build_basic_type(OpenApiTypes.STR)
def _get_response_for_code(self, path, method, serializer): serializer = force_instance(serializer) if not serializer: return {'description': 'No response body'} elif isinstance(serializer, serializers.ListSerializer): schema = self.resolve_serializer(method, serializer.child).ref elif is_serializer(serializer): component = self.resolve_serializer(method, serializer) if not component: return {'description': 'No response body'} schema = component.ref elif is_basic_type(serializer): schema = build_basic_type(serializer) elif isinstance(serializer, dict): # bypass processing and use given schema directly schema = serializer else: warn( f'could not resolve "{serializer}" for {method} {path}. Expected either ' f'a serializer or some supported override mechanism. defaulting to ' f'generic free-form object.') schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = 'Unspecified response body' if isinstance(serializer, serializers.ListSerializer) or is_list_view( path, method, self.view): # TODO i fear is_list_view is not covering all the cases schema = build_array_type(schema) paginator = self._get_paginator() if paginator: schema = paginator.get_paginated_response_schema(schema) return { 'content': { mt: { 'schema': schema } for mt in self.map_renderers(path, method) }, # Description is required by spec, but descriptions for each response code don't really # fit into our model. Description is therefore put into the higher level slots. # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject 'description': '' }
def _map_type_hint(self, method): hint = getattr(method, '_spectacular_annotation', None) or typing.get_type_hints(method).get('return') if is_serializer(hint) or is_field(hint): return self._map_serializer_field(force_instance(hint)) elif is_basic_type(hint): return build_basic_type(hint) elif getattr(hint, '__origin__', None) is typing.Union: if type(None) == hint.__args__[1] and len(hint.__args__) == 2: schema = build_basic_type(hint.__args__[0]) schema['nullable'] = True return schema else: warn(f'type hint {hint} not supported yet. defaulting to "string"') return build_basic_type(OpenApiTypes.STR) else: warn(f'type hint for function "{method.__name__}" is unknown. defaulting to string.') return build_basic_type(OpenApiTypes.STR)
def _get_request_body(self): # only unsafe methods can have a body if self.method not in ('PUT', 'PATCH', 'POST'): return None serializer = force_instance(self.get_request_serializer()) request_body_required = False if isinstance(serializer, serializers.ListSerializer): component = self.resolve_serializer(serializer.child, 'request') schema = build_array_type(component.ref) request_body_required = True elif is_serializer(serializer): component = self.resolve_serializer(serializer, 'request') if not component: # serializer is empty so skip content enumeration return None schema = component.ref if component.schema.get('required', []): request_body_required = True elif is_basic_type(serializer): schema = build_basic_type(serializer) if not schema: return None else: warn( f'could not resolve request body for {self.method} {self.path}. defaulting to generic ' 'free-form object. (maybe annotate a Serializer class?)' ) schema = { 'type': 'object', 'additionalProperties': {}, # https://github.com/swagger-api/swagger-codegen/issues/1318 'description': 'Unspecified request body', } request_body = { 'content': { request_media_types: {'schema': schema} for request_media_types in self.map_parsers() } } if request_body_required: request_body['required'] = request_body_required return request_body
def _map_serializer(self, auto_schema, direction, mapping): sub_components = [] for key, serializer_class in mapping.items(): sub_serializer = force_instance(serializer_class) resolved_sub_serializer = auto_schema.resolve_serializer( sub_serializer, direction) sub_components.append((key, resolved_sub_serializer.ref)) return { "oneOf": [ref for _, ref in sub_components], "discriminator": { "propertyName": self.target.type_field_name, "mapping": { resource_type: ref["$ref"] for resource_type, ref in sub_components }, }, }
def _map_response_type_hint(self, method): hint = get_override(method, 'field') or typing.get_type_hints(method).get('return') if is_serializer(hint) or is_field(hint): return self._map_serializer_field(force_instance(hint), 'response') elif is_basic_type(hint, allow_none=False): return build_basic_type(hint) elif getattr(hint, '__origin__', None) is typing.Union: if type(None) == hint.__args__[1] and len(hint.__args__) == 2: schema = build_basic_type(hint.__args__[0]) schema['nullable'] = True return schema else: warn(f'type hint {hint} not supported yet. defaulting to "string"') return build_basic_type(OpenApiTypes.STR) else: warn( f'type hint for function "{method.__name__}" is unknown. consider using ' f'a type hint or @extend_schema_field. defaulting to string.' ) return build_basic_type(OpenApiTypes.STR)
def _map_serializer(self, auto_schema, direction, mapping): sub_components = [] for key, serializer_class in mapping.items(): sub_serializer = force_instance(serializer_class) resolved_sub_serializer = auto_schema.resolve_serializer( sub_serializer, direction ) sub_components.append((key, resolved_sub_serializer.ref)) return { 'oneOf': [ref for _, ref in sub_components], 'discriminator': { 'propertyName': self.target.type_field_name, 'mapping': { resource_type: ref['$ref'] for resource_type, ref in sub_components } } }
def _get_implicit_sub_components(self, auto_schema, direction): sub_components = [] for sub_serializer in self.target.serializers: sub_serializer = force_instance(sub_serializer) resolved_sub_serializer = auto_schema.resolve_serializer( sub_serializer, direction) try: discriminator_field = sub_serializer.fields[ self.target.resource_type_field_name] resource_type = discriminator_field.to_representation(None) except: # noqa: E722 warn( f'sub-serializer {resolved_sub_serializer.name} of {self.target.component_name} ' f'must contain the discriminator field "{self.target.resource_type_field_name}". ' f'defaulting to sub-serializer name, but schema will likely not match the API.' ) resource_type = resolved_sub_serializer.name sub_components.append((resource_type, resolved_sub_serializer.ref)) return sub_components
def resolve_serializer(self, method, serializer) -> ResolvedComponent: assert is_serializer(serializer) serializer = force_instance(serializer) component = ResolvedComponent( name=self._get_serializer_name(method, serializer), type=ResolvedComponent.SCHEMA, object=serializer, ) if component in self.registry: return self.registry[component] # return component with schema self.registry.register(component) component.schema = self._map_serializer(method, serializer) # 3 cases: # 1. polymorphic container component -> use # 2. concrete component with properties -> use # 3. concrete component without properties -> prob. transactional so discard if 'oneOf' not in component.schema and not component.schema[ 'properties']: del self.registry[component] return ResolvedComponent(None, None) # sentinel return component
def _map_basic_serializer(self, serializer, direction): serializer = force_instance(serializer) required = set() properties = {} for field in serializer.fields.values(): if isinstance(field, serializers.HiddenField): continue if field.field_name in get_override(serializer, 'exclude_fields', []): continue schema = self._map_serializer_field(field, direction) # skip field if there is no schema for the direction if not schema: continue add_to_required = ( field.required or (schema.get('readOnly') and not spectacular_settings.COMPONENT_NO_READ_ONLY_REQUIRED)) if add_to_required: required.add(field.field_name) self._map_field_validators(field, schema) properties[field.field_name] = safe_ref(schema) if spectacular_settings.COMPONENT_SPLIT_PATCH: if self.method == 'PATCH' and direction == 'request': required = [] return build_object_type( properties=properties, required=required, description=get_doc(serializer.__class__), )
def test_force_instance(): assert isinstance(force_instance(serializers.CharField), serializers.CharField) assert force_instance(5) == 5 assert force_instance(dict) == dict
def _map_serializer_field(self, field, direction): meta = self._get_serializer_field_meta(field) if has_override(field, 'field'): override = get_override(field, 'field') if is_basic_type(override): schema = build_basic_type(override) elif isinstance(override, dict): schema = override else: schema = self._map_serializer_field(force_instance(override), direction) field_component_name = get_override(field, 'field_component_name') if field_component_name: component = ResolvedComponent( name=field_component_name, type=ResolvedComponent.SCHEMA, schema=schema, object=field, ) self.registry.register_on_missing(component) return append_meta(component.ref, meta) else: return append_meta(schema, meta) serializer_field_extension = OpenApiSerializerFieldExtension.get_match(field) if serializer_field_extension: schema = serializer_field_extension.map_serializer_field(self, direction) return append_meta(schema, meta) # nested serializer if isinstance(field, serializers.Serializer): component = self.resolve_serializer(field, direction) return append_meta(component.ref, meta) if component else None # nested serializer with many=True gets automatically replaced with ListSerializer if isinstance(field, serializers.ListSerializer): if is_serializer(field.child): component = self.resolve_serializer(field.child, direction) return append_meta(build_array_type(component.ref), meta) if component else None else: schema = self._map_serializer_field(field.child, direction) return append_meta(build_array_type(schema), meta) # Related fields. if isinstance(field, serializers.ManyRelatedField): schema = self._map_serializer_field(field.child_relation, direction) # remove hand-over initkwargs applying only to outer scope schema.pop('description', None) schema.pop('readOnly', None) return append_meta(build_array_type(schema), meta) if isinstance(field, serializers.PrimaryKeyRelatedField): # read_only fields do not have a Manager by design. go around and get field # from parent. also avoid calling Manager. __bool__ as it might be customized # to hit the database. if getattr(field, 'queryset', None) is not None: model_field = field.queryset.model._meta.pk else: if isinstance(field.parent, serializers.ManyRelatedField): relation_field = field.parent.parent.Meta.model._meta.get_field(field.parent.source) else: relation_field = field.parent.Meta.model._meta.get_field(field.source) model_field = relation_field.related_model._meta.pk # primary keys are usually non-editable (readOnly=True) and map_model_field correctly # signals that attribute. however this does not apply in the context of relations. schema = self._map_model_field(model_field, direction) schema.pop('readOnly', None) return append_meta(schema, meta) if isinstance(field, serializers.StringRelatedField): return append_meta(build_basic_type(OpenApiTypes.STR), meta) if isinstance(field, serializers.SlugRelatedField): return append_meta(build_basic_type(OpenApiTypes.STR), meta) if isinstance(field, serializers.HyperlinkedIdentityField): return append_meta(build_basic_type(OpenApiTypes.URI), meta) if isinstance(field, serializers.HyperlinkedRelatedField): return append_meta(build_basic_type(OpenApiTypes.URI), meta) if isinstance(field, serializers.MultipleChoiceField): return append_meta(build_array_type(build_choice_field(field)), meta) if isinstance(field, serializers.ChoiceField): return append_meta(build_choice_field(field), meta) if isinstance(field, serializers.ListField): if isinstance(field.child, _UnvalidatedField): return append_meta(build_array_type({}), meta) elif is_serializer(field.child): component = self.resolve_serializer(field.child, direction) return append_meta(build_array_type(component.ref), meta) if component else None else: schema = self._map_serializer_field(field.child, direction) return append_meta(build_array_type(schema), meta) # DateField and DateTimeField type is string if isinstance(field, serializers.DateField): return append_meta(build_basic_type(OpenApiTypes.DATE), meta) if isinstance(field, serializers.DateTimeField): return append_meta(build_basic_type(OpenApiTypes.DATETIME), meta) if isinstance(field, serializers.TimeField): return append_meta(build_basic_type(OpenApiTypes.TIME), meta) if isinstance(field, serializers.EmailField): return append_meta(build_basic_type(OpenApiTypes.EMAIL), meta) if isinstance(field, serializers.URLField): return append_meta(build_basic_type(OpenApiTypes.URI), meta) if isinstance(field, serializers.UUIDField): return append_meta(build_basic_type(OpenApiTypes.UUID), meta) if isinstance(field, serializers.DurationField): return append_meta(build_basic_type(OpenApiTypes.STR), meta) if isinstance(field, serializers.IPAddressField): # TODO this might be a DRF bug. protocol is not propagated to serializer although it # should have been. results in always 'both' (thus no format) if 'ipv4' == field.protocol.lower(): schema = build_basic_type(OpenApiTypes.IP4) elif 'ipv6' == field.protocol.lower(): schema = build_basic_type(OpenApiTypes.IP6) else: schema = build_basic_type(OpenApiTypes.STR) return append_meta(schema, meta) # DecimalField has multipleOf based on decimal_places if isinstance(field, serializers.DecimalField): if getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING): content = {**build_basic_type(OpenApiTypes.STR), 'format': 'decimal'} else: content = build_basic_type(OpenApiTypes.DECIMAL) if field.max_whole_digits: content['maximum'] = int(field.max_whole_digits * '9') + 1 content['minimum'] = -content['maximum'] self._map_min_max(field, content) return append_meta(content, meta) if isinstance(field, serializers.FloatField): content = build_basic_type(OpenApiTypes.FLOAT) self._map_min_max(field, content) return append_meta(content, meta) if isinstance(field, serializers.IntegerField): content = build_basic_type(OpenApiTypes.INT) self._map_min_max(field, content) # 2147483647 is max for int32_size, so we use int64 for format if int(content.get('maximum', 0)) > 2147483647 or int(content.get('minimum', 0)) > 2147483647: content['format'] = 'int64' return append_meta(content, meta) if isinstance(field, serializers.FileField): if spectacular_settings.COMPONENT_SPLIT_REQUEST and direction == 'request': content = build_basic_type(OpenApiTypes.BINARY) else: use_url = getattr(field, 'use_url', api_settings.UPLOADED_FILES_USE_URL) content = build_basic_type(OpenApiTypes.URI if use_url else OpenApiTypes.STR) return append_meta(content, meta) if isinstance(field, serializers.SerializerMethodField): method = getattr(field.parent, field.method_name) return append_meta(self._map_response_type_hint(method), meta) if anyisinstance(field, [serializers.BooleanField, serializers.NullBooleanField]): return append_meta(build_basic_type(OpenApiTypes.BOOL), meta) if isinstance(field, serializers.JSONField): return append_meta(build_basic_type(OpenApiTypes.OBJECT), meta) if anyisinstance(field, [serializers.DictField, serializers.HStoreField]): content = build_basic_type(OpenApiTypes.OBJECT) if not isinstance(field.child, _UnvalidatedField): content['additionalProperties'] = self._map_serializer_field(field.child, direction) return append_meta(content, meta) if isinstance(field, serializers.CharField): return append_meta(build_basic_type(OpenApiTypes.STR), meta) if isinstance(field, serializers.ReadOnlyField): # direct source from the serializer assert field.source_attrs, f'ReadOnlyField "{field}" needs a proper source' target = follow_field_source(field.parent.Meta.model, field.source_attrs) if callable(target): schema = self._map_response_type_hint(target) elif isinstance(target, models.Field): schema = self._map_model_field(target, direction) else: assert False, f'ReadOnlyField target "{field}" must be property or model field' return append_meta(schema, meta) # DRF was not able to match the model field to an explicit SerializerField and therefore # used its generic fallback serializer field that simply wraps the model field. if isinstance(field, serializers.ModelField): schema = self._map_model_field(field.model_field, direction) return append_meta(schema, meta) warn(f'could not resolve serializer field "{field}". defaulting to "string"') return append_meta(build_basic_type(OpenApiTypes.STR), meta)