-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
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
Does DRF support nested writable serializer with multipart/form-data? #7262
Comments
TL;DR: No, DRF doesn't support nested writable serializers with Nested serializers that are flagged |
@xordoquy I'd be very interested to hear the non-TLDR version of your comment above.
Is this just a left-over relic from the past? Looking at If all that is needed is to over-write the Thanks |
@tim-mccurrach the code should work with a nested "single" serializer - ie without
This would give something like It's a really tough topic to tackle and quite very little traction to get it working. |
@xordoquy Thanks for the detailed (and speedy) reply :) I wasn't really thinking about the BrowsableAPI, but what you've made written makes a lot of sense. |
@tim-mccurrach and anyone else stuck on this... you can technically get around the issue by sending For example: class A(Serializer):
pass
class B(Serializer):
nested_a = A(many=True, read_only=True)
nested_a_json = JsonField(write_only=True)
def validate_nested_a_json(self, value):
if not isinstance(value, list):
ValidationError("nested_a_json expects a list")
for item in value:
serializer = A(data=item)
serializer.is_valid(raise_exception=True)
return value @xordoquy I'd love for the framework to detect that the nested_a property has been provided as a stringified version instead of needing to split things out into read_only and write_only then having to handle the nesting manually... However, for the time being, do you see any issues with the approach above? |
We achieved this with a custom Parser that uses https://pypi.org/project/FormEncode/ . The browsable API does not work however as we only need this in a bulk endpoint. Below is our code, with a dirty hack to detect if files were uploaded: class MultipartFormencodeParser(parsers.MultiPartParser):
def parse(self, stream: Any, media_type: Any = None, parser_context: Any = None) -> Dict[str, Any]:
result = cast(parsers.DataAndFiles, super().parse(
stream,
media_type=media_type,
parser_context=parser_context
))
_data_keys: Set[str] = set(result.data.keys())
_file_keys: Set[str] = set(result.files.keys())
_intersect = _file_keys.intersection(_data_keys)
if len(_intersect) > 0:
raise ValidationError('files and data had intersection on keys: ' + str(_intersect))
# merge everything together
merged = QueryDict(mutable=True)
merged.update(result.data)
merged.update(result.files) # type: ignore
# decode it together
decoded_merged = variable_decode(merged)
parser_context['__JSON_AS_STRING__'] = True
if len(result.files) > 0:
# if we had at least one file put everything into files so we
# later know we had at least one file by running len(request.FILES)
parser_context['request'].META['REQUEST_HAD_FILES'] = True
return parsers.DataAndFiles(decoded_merged, {}) # type: ignore
else:
# just put it into data, doesnt matter really otherwise
return parsers.DataAndFiles(decoded_merged, {}) # type: ignore |
The following example seems to work for me via API (not html renderer). However it should be sent without {
'c[0]b[0]a.text': 'my-text',
'c[0]b[1]a.text': 'my-text2',
} Serializers: class A(serializers.Serializer):
text = serializers.CharField()
class B(serializers.Serializer):
a = A()
class C(serializers.Serializer):
b = B(many=True)
class D(serializers.Serializer):
c = C(many=True)
def validate(self, data):
print(data)
return data |
did you check drf-writable-nested? |
No i didn't |
Hi, Do you know why this works?
|
I am not sure, it might depend on your serializer ? In your case it might not be a nested serializer but just a class A(serializers.Serializer):
attr = ListField(CharField()) |
Yes, you are right, I was just pointing out this other case when it is sent a list of strings. |
Adding my contribution into this in case anyone face this issue in the future. Here is a simple example using Two models with a ManyToMany relation using a through Model. Models: class Invoice(models.Model):
date = models.DateField(default=date.today)
amount = models.FloatField(default=0)
image = models.ImageField(upload_to='images/', blank=True, null=True)
products = models.ManyToManyField('Product', through='InvoiceProduct')
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.FloatField(default=0)
class InvoiceProduct(models.Model):
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='invoiceproduct_set')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField(default=0) Serializers: class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
class InvoiceProductSerializer(serializers.ModelSerializer):
invoice = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = InvoiceProduct
fields = '__all__'
class InvoiceSerializer(serializers.ModelSerializer):
products = InvoiceProductSerializer(source='invoiceproduct_set', many=True)
@transaction.atomic
def create(self, validated_data):
items = validated_data.pop('invoiceproduct_set')
invoice = self.Meta.model.objects.create(validated_data)
for item in items:
item['invoice'] = invoice
InvoiceProduct.objects.create(**item) Viewset: class MultipartNestedSupportMixin:
def transform_request_data(self, data):
# transform data sctructure to dictionnary
force_dict_data = data
if type(force_dict_data) == QueryDict:
force_dict_data = force_dict_data.dict()
# transform JSON string to dictionnary for each many field
serializer = self.get_serializer()
#print(force_dict_data)
for key, value in serializer.get_fields().items():
if isinstance(value, serializers.ListSerializer) or isinstance(value, serializers.ModelSerializer):
if key in force_dict_data and type(force_dict_data[key]) == str:
if force_dict_data[key] == '':
force_dict_data[key] = None
else:
try:
force_dict_data[key] = json.loads(force_dict_data[key])
except:
pass
return force_dict_data
def create(self, request, *args, **kwargs):
force_dict_data = self.transform_request_data(request.data)
serializer = self.get_serializer(data=force_dict_data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def update(self, request, *args, **kwargs):
force_dict_data = self.transform_request_data(request.data)
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=force_dict_data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
class InvoiceViewSet(MultipartNestedSupportMixin, viewsets.ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer With this you shloud be able to send multipart formdata with files and nested objects as JSON text : I managed to create a Mixin that can handle the creation of the Nested Objects for any similar use case as above, you can use this : class WritableNestedThroughMixin():
@transaction.atomic
def create(self, validated_data):
validated_data_no_nested = validated_data.copy()
for key, value in self.get_fields().items():
if isinstance(value, serializers.ListSerializer):
validated_data_no_nested.pop(value.source if value.source is not None else key)
main_obj = self.Meta.model.objects.create(**validated_data_no_nested)
for key, value in self.get_fields().items():
if isinstance(value, serializers.ListSerializer):
items = validated_data.pop(value.source if value.source is not None else key, None)
if items is not None:
ItemModel = value.child.Meta.model
link_field = None
for field in ItemModel._meta.fields:
if field.related_model == self.Meta.model:
link_field = field.name
#print('### PARENT FIELD : ', parent_field)
if link_field is not None:
for item in items:
item[link_field] = main_obj
ItemModel.objects.create(**item)
return main_obj
#You can then use :
class InvoiceSerializer(WritableNestedThroughMixin, serializers.ModelSerializer):
products = InvoiceProductSerializer(source='invoiceproduct_set', many=True) Hope this helps someone in the future. feel free to make any remarks or suggestions about the code or how to improve things in it. |
@merabtenei why is this @transaction.atomic? I will try to use the solution and I have similar questions on the stackoverflow question1 and question2 but haven't got any answers to the questions. My other question is this issue seems to be closed. Is it solved in any other form? Or there is no plan to solve this issue? Thank you. |
This will revert all changes if any exception is raised. Nested objects require many instances to be created, we need to make sure that either all instances are created or none of them. |
I already customized the update method in my viewset, hence I only needed to add |
Checklist
master
branch of Django REST framework.Hello,
I'm trying to upload a file with some nested data via
POST
andcontent-type:multipart/form-data
, no matter how I'm sending the value for the nested field as a text or JSON my serializer always rasing the exception for the nested field asThis field is required
despite I'm sure it is being correctly mapped torequest.data
- fields and values are as expected - and passed to the parent serializer, I have even tried to write a parser or to use a package but nothing changes, for some reason the nested field is always dropped at validation.The text was updated successfully, but these errors were encountered: