multipart/form-data
with Laravel Validation Rules with proper JSON data types
January 1st ʼ22 7 months ago
2 min 262 words
This article co-written by me and Oğuzhan Karacabay, also available in Turkish, co-published on Medium.
When you want both upload files and send JSON
payload from a Frontend Single Page Application
to your Laravel Backend API
, you send it with a multipart/form-data
encoding.
A typical example of doing this regardless of Frontend Frameworks would be:
postMultipartFormData(form) {
let formData = new FormData()
formData.append('file', form.file)
formData.append('id', form.id) // 1234
formData.append('reason_type', form.reason_type) // 3
formData.append('rate', form.rate) // 10.15
formData.append('fee', form.fee * 100) // 1000 * 100
formData.append('tax', form.tax * 100) // 190 * 100
formData.append('description', form.description) // some description
formData.append('action_date', form.action_date) // 2022-01-07 17:52:06
return apiClient
.post('/post/multipart-formdata', formData, {
headers: {'Content-Type': 'multipart/form-data'}
})
.then(response => { return response })
.catch(err => { throw err })
}
As a good Laravel developer, you want to validate this payload with a Laravel Request
class before it comes to the Controller
.
The Laravel Controller might look more or less like this:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\MultipartFormRequest;
use App\Http\Resources\MultipartFormResource;
class MultipartFormController extends Controller
{
public function store(MultipartFormRequest $request): MultipartFormResource
{
$file = $request->file('file');
$path = '/your/path';
$filename = 'filename.xlsx';
// Save file to a local or remote file bucket
$file->storeAs($path, $filename, ['disk' => 's3-public']);
// Create a model with file url
$yourModel = YourModel::create(array_merge($request->validated(), [
'file_url' => $path . $filename,
]));
// Return a resource with your newly generated model
return new MultipartFormResource($yourModel);
}
}
and an example Laravel Request class would probably look something like this:
<?php
namespace App\Http\Requests;
use App\Enums\PromissoryNoteReasonType;
use BenSampo\Enum\Rules\EnumValue;
use Illuminate\Foundation\Http\FormRequest;
class MultipartFormRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'bank_id' => ['bail', 'required', 'numeric', 'exists:banks,id'],
'promissory_note_reason' => ['bail', 'required', new EnumValue(PromissoryNoteReasonType::class)],
'interest_rate' => ['bail', 'required', 'between:0,99.9999'],
'fixed_fee' => ['bail', 'required', 'integer'],
'tax' => ['bail', 'required', 'integer'],
'description' => ['bail', 'required', 'string'],
'action_date' => ['bail', 'required', 'date'],
'file' => ['bail', 'required', 'file', 'mimes:xls,xlsx',
],
];
}
}
So far, everything went perfectly, but when you run this code, you will notice that the data you sent did not pass the validation. Dig deeper and you’ll see that the data coming into your Laravel API
is very different from what we expected:
array:8 [▼
"file" => "file-content"
"id" => "123"
"reason_type" => "3"
"rate" => "10.15"
"fee" => "10000"
"tax" => "1900"
"description" => "request description"
"action_date" => "2020-10-07 17:52:06"
]
So you noticed that multipart/formdata
, in addition to its ability to both, send binary files and JSON payload, converts all data types to strings,.
After this step, you can change your validation rules assuming that the incoming data will be all string types, you can do your parse gymnastics
to fully validate string-type values.
If the FormData
object can only send data of string types and we must use a FormData
object to upload files, we will use it in this way. Of course, by first converting all the data to be sent to a JSON string.
After adding the file we want to upload to FormData
(I.), we convert all the payload to a JSON
string with the stringify()
function. (II.)
Thus, we have a FormData
object containing only the file and payload to send to the Backend API. (III.)
We used parseFloat()
function because the stringify()
function cannot parse float types to a JSON
string with correct data type. (IV.)
postMultipartFormData(form) {
let formData = new FormData()
formData.append('file', form.file) // I.
let payload = JSON.stringify({
id: form.id,
reason_type: form.reason_type,
rate: parseFloat(form.rate), // IV.
fee: form.fee * 100,
tax: form.tax * 100,
description: form.description,
action_date: form.action_date
}); // II.
formData.append('payload', payload) // III.
return apiClient
.post('/post/multipart-formdata', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
.then(response => { return response })
.catch(err => { throw err })
}
We need to convert the JSON
string coming to the Backend API side into a JSON
Payload just before passing it through the validation rules. The prepareForValidation()
method in Laravel’s Request classes is there for just that.
/**
* Prepare the data for validation.
*
* @return void
*
* @throws \JsonException
*/
protected function prepareForValidation(): void
{
$this->merge(json_decode($this->payload, true, 512, JSON_THROW_ON_ERROR));
}
After converting the JSON
String to JSON
Payload using the json_decode()
function in the prepareForValidation()
method, we combine it with other Request data with merge()
which is another method of Laravel Request
classes.
array:7 [▼
"id" => 123
"reason_type" => 3
"rate" => 10.15
"fee" => 10000
"tax" => 1900
"description" => "request description"
"action_date" => "2020-10-07 17:52:06"
]
Thus, our data was cast into appropriate data types and passed all the validation rules. So there is no need to change the validation rules or do any additional validation gymnastics.