Angular调用添加商品API返回400错误,请求技术排查
问题现象
有一个向数据库添加商品的API,Postman测试正常,但Angular表单提交时返回400错误,后端抛出JSON反序列化异常:
JSON parse error: Cannot deserialize value of type
com.example.backendapp.entities.Imagefrom Array value (tokenJsonToken.START_ARRAY); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of typecom.example.backendapp.entities.Imagefrom Array value (tokenJsonToken.START_ARRAY) at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 151] (through reference chain: com.example.backendapp.DTOs.ItemAddDTO["image"])
已核对参数名、API端点,移除图片上传功能仍无效,其他接口运行正常。
代码片段
Angular 代码
Service 方法
addItem(item:FormData):Observable<any>{ //itemAdd return this.http.post<ItemAdd>(this.url+this.endpoint,item); }
表单与Item定义
addItemForm = new FormGroup({ brand: new FormControl(''), name: new FormControl(''), serialNumber: new FormControl(''), price: new FormControl(''), generalCategory: new FormControl(''), category: new FormControl(''), descriere: new FormControl(''), quantity: new FormControl(''), percentageSale: new FormControl(''), image: new FormControl('') }); item: ItemAdd = { brand: "", name: "", serialNumber: "", price: "", generalCategory: "", category: "", descriere: "", quantity: "", percentSale: "", image: [] // 数组类型 }
添加商品函数
addItem() { this.item.brand = this.addItemForm.value.brand; this.item.name = this.addItemForm.value.name; this.item.serialNumber = this.addItemForm.value.serialNumber; this.item.price = this.addItemForm.value.price; this.item.generalCategory = this.addItemForm.value.generalCategory; this.item.category = this.addItemForm.value.category; this.item.descriere = this.addItemForm.value.descriere; this.item.quantity = this.addItemForm.value.quantity; this.item.percentSale = this.addItemForm.value.percentageSale; const itemFormData = this.createFormData(this.item) this.dialService.addItem(itemFormData).subscribe(response => { this.dialogRef.close(); this.toast.showToast("Item succesfully created", "info"); console.log("item added") console.log(response); }); }
FormData 创建函数
createFormData(item: ItemAdd): FormData { const formData = new FormData(); formData.append('item', new Blob([JSON.stringify(item)], {type: 'application/json'})); for (var image = 0; image < item.image.length; image++) { formData.append('image', item.image[image].file, item.image[image].file.name) } return formData; }
请求与响应头
请求头
Accept: application/json, text/plain, */* Accept-Encoding: gzip, deflate, br Accept-Language: ro-RO,ro;q=0.9,en-US;q=0.8,en;q=0.7 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbkB5YWhvby5jb20iLCJpYXQiOjE2ODA5NDk0ODcsImV4cCI6MTY4MTEyMjI4N30.nZgaAbjFRYZNZFsfHGoA9DrTLCUuC4N26q5VtXovS1g Connection: keep-alive Content-Length: 473142 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryw60j7sVWdlQukUvr Host: localhost:8081 Origin: http://localhost:4200 Referer: http://localhost:4200/ sec-ch-ua: "Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
响应头
access-control-allow-methods: PUT, GET, HEAD, POST, DELETE, OPTIONS access-control-allow-origin: http://localhost:4200 Cache-Control: no-cache, no-store, max-age=0, must-revalidate Connection: close Content-Type: application/json Date: Sat, 08 Apr 2023 13:25:15 GMT Expires: 0 Pragma: no-cache Transfer-Encoding: chunked Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1; mode=block
后端代码
Controller 方法
@PostMapping("/items") public Item createItem(@RequestPart("item") ItemAddDTO item,@RequestPart("image") MultipartFile file) throws IOException { return itemService.saveItem(item,file); }
Service 方法
public Item saveItem(ItemAddDTO itemAddDTO,MultipartFile file) throws IOException { Item item = this.modelMapper.map(itemAddDTO, Item.class); item.setDateOfAdd(new Date()); item.setImage(imageService.uploadImageToFile(file)); itemRepository.save(item); return item; }
问题根源
后端ItemAddDTO的image字段是单个Image对象类型,但前端ItemAdd的image是数组类型,且序列化item为JSON时把空数组[]传了过去。后端Jackson尝试将数组反序列化为单个对象,导致类型不匹配,抛出400错误。
另外,图片已经通过单独的RequestPart传递,不需要在item的JSON中包含image字段。
解决方案
修改前端代码,移除ItemAdd中的image字段,或者在序列化时排除该字段:
方案1:修改ItemAdd定义
item: ItemAdd = { brand: "", name: "", serialNumber: "", price: "", generalCategory: "", category: "", descriere: "", quantity: "", percentSale: "" // 移除image字段 }
方案2:序列化时排除image字段
修改createFormData函数,序列化时忽略image:
createFormData(item: ItemAdd): FormData { const formData = new FormData(); // 深拷贝item并删除image属性 const itemWithoutImage = {...item}; delete itemWithoutImage.image; formData.append('item', new Blob([JSON.stringify(itemWithoutImage)], {type: 'application/json'})); for (var image = 0; image < item.image.length; image++) { formData.append('image', item.image[image].file, item.image[image].file.name) } return formData; }
这样后端就能正确解析ItemAddDTO,不会出现类型不匹配的问题。
内容的提问来源于stack exchange,提问作者DanM

