So im kinda new to angular and django, but im trying to create a web app kinda similar to Amazon, where the user is able to log in and browse different listings from other user, create his own listings and do basic CRUD operations on his own listings. When creating a listing the user needs to add a title, description, price, location choose a category and subcategory, add values to the attributes, and he can attach images as files. In the database both categories and subcategories have their own "attributes" whose values also need to be inputed by the user. For an example a category is lets say "Pet", that will have a attribute lets say "Age", and for a subcategory "Dog" there will be an attribute "Breed". I found a tutorial online for adding images, and basically im sending them as files, and saving them in a folder on backend. When I try to add a listing, everything works fine except that the attributes are never added to the database. I added some console logs on front to see if im getting them and sending as json and I do, so I think that the problem is on backend so I tried to add some prints on backend to try and see whats the problem, and I think its because of how formData handles JSON I tried googling, Gemini, ChatGpt, Phind, nothing helped so here I am Here is my ts file @Component({ standalone: true, selector: 'app-add-listing', templateUrl: './add-listing.component.html', styleUrls: ['./add-listing.component.scss'], imports: [ CommonModule, MatFormField, MatLabel, MatInputModule, MatSelect, MatOption, ReactiveFormsModule, TranslateModule ] }) export class AddListingComponent implements OnInit { listingForm: FormGroup; categories: any[] = []; subcategories: string[] = []; selectedAttributes: string[] = []; images: File[] = []; imagePreviews: string[] = []; constructor( private fb: FormBuilder, private authService: AuthService, @Inject(ListingService) private listingService: ListingService, private toastr: ToastrService, private router: Router ) { this.listingForm = this.fb.group({ user_id: [this.authService.getUserId()], title: ['', [Validators.required]], description: ['', [Validators.required]], price: ['', [Validators.required]], location: [''], category: ['', Validators.required], subcategory: ['', Validators.required], attributes: this.fb.array([]), }); } ngOnInit(): void { this.listingService.getCategories().subscribe({ next: (categories: any[]) => { this.categories = categories.map((category) => ({ name: category.category.category_name, attributes: category.attributes.map((attr: any) => attr.attribute_name), subcategories: category.subcategories.map((subcategory: any) => ({ name: subcategory.subcategory_name, attributes: subcategory.attributes.map((attr: any) => attr.attribute_name), })), })); }, error: (error) => { console.error('Error fetching categories:', error); }, }); } get attributes(): FormArray { return this.listingForm.get('attributes') as FormArray; } onCategoryChange(event: any): void { const category = this.categories.find((cat) => cat.name === event.value); this.subcategories = category ? category.subcategories.map((sub: any) => sub.name) : []; this.selectedAttributes = category ? category.attributes : []; this.updateAttributesFormArray(this.selectedAttributes); } onSubCategoryChange(event: any): void { const selectedCategory = this.categories.find((cat) => cat.subcategories.some((sub: any) => sub.name === event.value) ); const selectedSubcategory = selectedCategory?.subcategories.find((sub: any) => sub.name === event.value); const parentAttributes = selectedCategory?.attributes || []; const subcategoryAttributes = selectedSubcategory?.attributes || []; this.selectedAttributes = [...new Set([...parentAttributes, ...subcategoryAttributes])]; this.updateAttributesFormArray(this.selectedAttributes); } private updateAttributesFormArray(attributes: string[]): void { this.attributes.clear(); attributes.forEach((attr) => { this.attributes.push( this.fb.group({ attribute_name: [attr, Validators.required], value: ['', Validators.required], }) ); }); } onImageChange(event: Event): void { const files = (event.target as HTMLInputElement).files; if (files && this.images.length + files.length <= 5) { Array.from(files).forEach((file) => { this.images.push(file); const reader = new FileReader(); reader.onload = () => this.imagePreviews.push(reader.result as string); reader.readAsDataURL(file); }); } else { this.toastr.error('You can upload a maximum of 5 images.'); } } removeImage(index: number): void { this.images.splice(index, 1); this.imagePreviews.splice(index, 1); } onSubmit(): void { if (this.listingForm.valid) { const formData = new FormData(); formData.append('data', JSON.stringify(this.listingForm.value)); this.images.forEach((image) => { formData.append('images', image); }); this.listingService.createListing(formData).subscribe({ next: () => { this.toastr.success('Listing created successfully'); this.router.navigate(['/user', this.authService.getUserId()]); }, error: (error) => { console.error('Error creating listing:', error); }, }); } } } and here is the backend code for creating a listing from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import AllowAny from drf_yasg.utils import swagger_auto_schema from ..serializers.listing_serializers import ListingCreateSerializer from merkatapp.models import User from rest_framework.parsers import MultiPartParser, JSONParser from django.http import QueryDict import json class ListingCreateAPI(APIView): permission_classes = [AllowAny] parser_classes = [MultiPartParser, JSONParser] # Allow JSON and multipart/form-data @swagger_auto_schema( request_body=ListingCreateSerializer, responses={ 201: "Listing created successfully.", 400: "Bad Request", }, ) def post(self, request, *args, **kwargs): print("-------------------------------------------\n") print("Request data:", request.data) print("-------------------------------------------\n") # Create a mutable copy of request.data data_copy = request.data.dict() if isinstance(request.data, QueryDict) else request.data print("-------------------------------------------\n") print("Request data copy:", data_copy) print("-------------------------------------------\n") # Extract and parse the JSON payload from the 'data' field if 'data' in data_copy: try: parsed_data = json.loads(data_copy['data']) data_copy = parsed_data # Merge parsed JSON data into data_copy print("Parsed and merged data:", data_copy) except json.JSONDecodeError as e: return Response({"error": f"Invalid JSON in 'data': {str(e)}"}, status=status.HTTP_400_BAD_REQUEST) print("-------------------------------------------\n") print("Request data after parsing:", data_copy) print("-------------------------------------------\n") # Reconstruct the request data as a QueryDict mutable_data = QueryDict('', mutable=True) mutable_data.update(data_copy) print("-------------------------------------------\n") print("Request data images:", request.FILES.getlist('images')) print("-------------------------------------------\n") mutable_data.setlist('images', request.FILES.getlist('images')) # Add images as a list print("-------------------------------------------\n") print("Mutable data:", mutable_data) print("-------------------------------------------\n") serializer = ListingCreateSerializer(data=mutable_data) print("-------------------------------------------\n") print("Serializer data:", serializer) print("-------------------------------------------\n") if serializer.is_valid(): user_id = serializer.validated_data.pop('user_id', None) try: user = User.objects.get(user_id=user_id) except User.DoesNotExist: return Response({"error": f"User with ID {user_id} does not exist."}, status=status.HTTP_400_BAD_REQUEST) serializer.save(user=user) return Response({"message": "Listing created successfully."}, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) as you can see I added some prints for debugging, and when trying to create a listing here what those prints printed Request data: <QueryDict: {'data': ['{"user_id":10,"title":"Testing","description":"Creating a listing","price":100,"location":"Sweden","category":"Video igre","subcategory":"Kompjuter","attributes":[{"attribute_name":"Platforma (PC, konzola, mobilni)","value":"PC"},{"attribute_name":"Žanr","value":"Action"}]}'], 'images': [<InMemoryUploadedFile: download (2).jpg (image/jpeg)>, <InMemoryUploadedFile: download (1).jpg (image/jpeg)>]}> ------------------------------------------- ------------------------------------------- Request data copy: {'data': '{"user_id":10,"title":"Testing","description":"Creating a listing","price":100,"location":"Sweden","category":"Video igre","subcategory":"Kompjuter","attributes":[{"attribute_name":"Platforma (PC, konzola, mobilni)","value":"PC"},{"attribute_name":"Žanr","value":"Action"}]}', 'images': <InMemoryUploadedFile: download (1).jpg (image/jpeg)>} ------------------------------------------- Parsed and merged data: {'user_id': 10, 'title': 'Testing', 'description': 'Creating a listing', 'price': 100, 'location': 'Sweden', 'category': 'Video igre', 'subcategory': 'Kompjuter', 'attributes': [{'attribute_name': 'Platforma (PC, konzola, mobilni)', 'value': 'PC'}, {'attribute_name': 'Žanr', 'value': 'Action'}]} ------------------------------------------- Request data after parsing: {'user_id': 10, 'title': 'Testing', 'description': 'Creating a listing', 'price': 100, 'location': 'Sweden', 'category': 'Video igre', 'subcategory': 'Kompjuter', 'attributes': [{'attribute_name': 'Platforma (PC, konzola, mobilni)', 'value': 'PC'}, {'attribute_name': 'Žanr', 'value': 'Action'}]} ------------------------------------------- ------------------------------------------- Request data images: [<InMemoryUploadedFile: download (2).jpg (image/jpeg)>, <InMemoryUploadedFile: download (1).jpg (image/jpeg)>] ------------------------------------------- ------------------------------------------- Mutable data: <QueryDict: {'user_id': [10], 'title': ['Testing'], 'description': ['Creating a listing'], 'price': [100], 'location': ['Sweden'], 'category': ['Video igre'], 'subcategory': ['Kompjuter'], 'attributes': [[{'attribute_name': 'Platforma (PC, konzola, mobilni)', 'value': 'PC'}, {'attribute_name': 'Žanr', 'value': 'Action'}]], 'images': [<InMemoryUploadedFile: download (2).jpg (image/jpeg)>, <InMemoryUploadedFile: download (1).jpg (image/jpeg)>]}> ------------------------------------------- ------------------------------------------- Serializer data: ListingCreateSerializer(data=<QueryDict: {'user_id': [10], 'title': ['Testing'], 'description': ['Creating a listing'], 'price': [100], 'location': ['Sweden'], 'category': ['Video igre'], 'subcategory': ['Kompjuter'], 'attributes': [[{'attribute_name': 'Platforma (PC, konzola, mobilni)', 'value': 'PC'}, {'attribute_name': 'Žanr', 'value': 'Action'}]], 'images': [<InMemoryUploadedFile: download (2).jpg (image/jpeg)>, <InMemoryUploadedFile: download (1).jpg (image/jpeg)>]}>): user_id = IntegerField() category = CharField() subcategory = CharField(allow_null=True, required=False) title = CharField(max_length=1024) description = CharField(style={'base_template': 'textarea.html'}) price = DecimalField(decimal_places=0, max_digits=8) location = CharField(allow_blank=True, allow_null=True, max_length=1024, required=False) attributes = ListingAttributeInputSerializer(many=True, required=False): attribute_name = CharField() value = CharField() images = ListField(allow_empty=True, child=ImageField(), required=False, write_only=True) i also tried to add a listing trough swagger and it worked there, here is how the request body looks like on swagger { user_id* integer title: User id category* string title: Category minLength: 1 subcategory string title: Subcategory minLength: 1 x-nullable: true title* string title: Title maxLength: 1024 minLength: 1 description* string title: Description minLength: 1 price* string($decimal) title: Price location string title: Location maxLength: 1024 x-nullable: true attributes [ListingAttributeInput{ attribute_name* string title: Attribute name minLength: 1 value* string title: Value minLength: 1 }] images [string($uri) readOnly: true] } any tips, tricks, hints would be very welcome, thanks in advance Continue reading...