Skip to content

Instantly share code, notes, and snippets.

@ghinda
Last active September 17, 2024 05:22
Show Gist options
  • Save ghinda/8442a57f22099bdb2e34 to your computer and use it in GitHub Desktop.
Save ghinda/8442a57f22099bdb2e34 to your computer and use it in GitHub Desktop.
JavaScript Object to FormData, with support for nested objects, arrays and File objects. Includes Angular.js usage.
// takes a {} object and returns a FormData object
var objectToFormData = function(obj, form, namespace) {
var fd = form || new FormData();
var formKey;
for(var property in obj) {
if(obj.hasOwnProperty(property)) {
if(namespace) {
formKey = namespace + '[' + property + ']';
} else {
formKey = property;
}
// if the property is an object, but not a File,
// use recursivity.
if(typeof obj[property] === 'object' && !(obj[property] instanceof File)) {
objectToFormData(obj[property], fd, property);
} else {
// if it's a string or a File object
fd.append(formKey, obj[property]);
}
}
}
return fd;
};
// usage example
var z = objectToFormData({
obj: {
prop: 'property value'
},
arr: [
'one',
'two',
'three',
new File([''], '')
],
file: new File([''], '')
});
var xhr = new XMLHttpRequest;
xhr.open('POST', '/', true);
xhr.send(z);
// usage for Angular.js
// wrap object to formdata method,
// to use it as a transform with angular's http.
var formDataTransform = function(data, headersGetter) {
// we need to set Content-Type to undefined,
// to make the browser set it to multipart/form-data
// and fill in the correct *boundary*.
// setting Content-Type to multipart/form-data manually
// will fail to fill in the boundary parameter of the request.
headersGetter()['Content-Type'] = undefined;
return objectToFormData(data);
};
$http({
method: 'POST',
url: '/',
transformRequest: formDataTransform,
data: { your_object: {} }
})
.success(function(res) {});
@EngrKhizarIqbal
Copy link

EngrKhizarIqbal commented Dec 31, 2017

You saved my life. Thanks a lot to all of them who contributed in the gist.

@donjajo
Copy link

donjajo commented Jan 4, 2018

Live saving!!!
Thank you! :)

@RocKhalil
Copy link

There's an issue with Arrays;
You'll need to add a condition for array, something like this:

if (obj[property] instanceof Date) {
  fd.append(formKey, obj[property].toISOString());
} else if (obj[property] instanceof Array) {
  formKey = formKey + '[]'
  obj[property].forEach(element => {
    fd.append(formKey, obj[property]);
  })
} else if (typeof obj[property] === 'object' && !(obj[property] instanceof File)) {
  this.toFormData(obj[property], fd, formKey);
} else { // if it's a string or a File object
  fd.append(formKey, obj[property]);
}

@Mds92
Copy link

Mds92 commented Mar 20, 2018

Full version with supporting array

export class Utility {      
    public static convertModelToFormData(model: any, form: FormData = null, namespace = ''): FormData {
        let formData = form || new FormData();
        let formKey;

        for (let propertyName in model) {
            if (!model.hasOwnProperty(propertyName) || !model[propertyName]) continue;
            let formKey = namespace ? `${namespace}[${propertyName}]` : propertyName;
            if (model[propertyName] instanceof Date)
                formData.append(formKey, model[propertyName].toISOString());
            else if (model[propertyName] instanceof Array) {
                model[propertyName].forEach((element, index) => {
                    const tempFormKey = `${formKey}[${index}]`;
                    this.convertModelToFormData(element, formData, tempFormKey);
                });
            }
            else if (typeof model[propertyName] === 'object' && !(model[propertyName] instanceof File))
                this.convertModelToFormData(model[propertyName], formData, formKey);
            else
                formData.append(formKey, model[propertyName].toString());
        }
        return formData;
    }
}

@Flexiink
Copy link

Mds92 Thank you! :)

@RudeySH
Copy link

RudeySH commented May 29, 2018

@RocKhalil The array handling you're suggesting is wrong, and not even needed. What if the array contains objects? The array handling should recursively call objectToFormData for each element instead of immediately appending the values to the formdata. Which is exactly what the object condition is already doing. And guess what? An array is an object. So you don't need an array condition at all!

Copy link

ghost commented Jun 12, 2018

Mds92 Thank you! :)

@humoyun
Copy link

humoyun commented Aug 22, 2018

@Mds92 Awesome. Thank you!

@denysloshkarev
Copy link

denysloshkarev commented Sep 29, 2018

I'm little change previous class to fix of adding of Array with basic type's items like numbers to FormData. Also this one is good to use for convert the Vue.js Observer objects to the FormData

export default class Helpers {
    static convertModelToFormData(data = {}, form = null, namespace = '') {
        let files = {};
        let model = {};
        for (let propertyName in data) {
            if (data.hasOwnProperty(propertyName) && data[propertyName] instanceof File) {
                files[propertyName] = data[propertyName]
            } else {
                model[propertyName] = data[propertyName]
            }
        }

        model = JSON.parse(JSON.stringify(model))
        let formData = form || new FormData();

        for (let propertyName in model) {
            if (!model.hasOwnProperty(propertyName) || !model[propertyName]) continue;
            let formKey = namespace ? `${namespace}[${propertyName}]` : propertyName;
            if (model[propertyName] instanceof Date)
                formData.append(formKey, model[propertyName].toISOString());
            else if (model[propertyName] instanceof File) {
                formData.append(formKey, model[propertyName]);
            }
            else if (model[propertyName] instanceof Array) {
                model[propertyName].forEach((element, index) => {
                    const tempFormKey = `${formKey}[${index}]`;
                    if (typeof element === 'object') this.convertModelToFormData(element, formData, tempFormKey);
                    else formData.append(tempFormKey, element.toString());
                });
            }
            else if (typeof model[propertyName] === 'object' && !(model[propertyName] instanceof File))
                this.convertModelToFormData(model[propertyName], formData, formKey);
            else {
                formData.append(formKey, model[propertyName].toString());
            }
        }

        for (let propertyName in files) {
            if (files.hasOwnProperty(propertyName)) {
                formData.append(propertyName, files[propertyName]);
            }
        }
        return formData;
    }
}

@MilosStanic
Copy link

Here's a TypeScript version:

function createFormData(object: Object, form?: FormData, namespace?: string): FormData {
    const formData = form || new FormData();
    for (let property in object) {
        if (!object.hasOwnProperty(property) || !object[property]) {
            continue;
        }
        const formKey = namespace ? `${namespace}[${property}]` : property;
        if (object[property] instanceof Date) {
            formData.append(formKey, object[property].toISOString());
        } else if (typeof object[property] === 'object' && !(object[property] instanceof File)) {
            createFormData(object[property], formData, formKey);
        } else {
            formData.append(formKey, object[property]);
        }
    }
    return formData;
}

@MidnightDesign I had to make a change in the first if of your code, because it was ignoring false booleans:

if (!object.hasOwnProperty(property)/*  || !object[property] */) {
            continue;
        }

@zernie
Copy link

zernie commented Dec 9, 2018

@denisloshkarev I had to remove model = JSON.parse(JSON.stringify(model)); line, because it converted nested File instances into objects.

@Peyu
Copy link

Peyu commented Jan 9, 2019

When used with some complex objects, sometimes you get some values concateneted. To prevent this behavior I add some lines at the last "else" statement:

                   if (obj[property] instanceof Date) {
                        ...
                        ...
                        } else { // if it's a string or a File object
    
                        var alreadySaved = false;
                        for (var pair of fd.entries()) {
                            if((pair[0] == formKey &&  obj[property] == pair[1])){
                                alreadySaved = true;
                            }; 
                        }

                        if(!alreadySaved){
                            fd.append(formKey, obj[property]);
                        }
                         
                    }`

@bshafiei-ir
Copy link

Here's a TypeScript version:

function createFormData(object: Object, form?: FormData, namespace?: string): FormData {
    const formData = form || new FormData();
    for (let property in object) {
        if (!object.hasOwnProperty(property) || !object[property]) {
            continue;
        }
        const formKey = namespace ? `${namespace}[${property}]` : property;
        if (object[property] instanceof Date) {
            formData.append(formKey, object[property].toISOString());
        } else if (typeof object[property] === 'object' && !(object[property] instanceof File)) {
            createFormData(object[property], formData, formKey);
        } else {
            formData.append(formKey, object[property]);
        }
    }
    return formData;
}

Good job man.
Thanks.

@cdrandin
Copy link

cdrandin commented Sep 13, 2019

Also, in case it isn't clear and people want to pass other file-like data or binary stuff. Replace File with Blob.

@avrail
Copy link

avrail commented Sep 20, 2019

if you have a complex object that contains file will not work
you need to do something like this


 // if the property is an object, but not a File, use recursivity.
                if (obj[property] instanceof Date) {
                    fd.append(formKey, obj[property].toISOString());
                }
                else if (typeof obj[property] === 'object' && !(obj[property] instanceof File)) {
                    this.toFormData(obj[property], fd, formKey);
                } else {
                    if (obj[property] instanceof File) {
                        //to add file.
                        formKey = formKey.replace('[' + property + ']', '.' + property);
                    }
                    // if it's a string or a File object
                    fd.append(formKey, obj[property]);
                }

@huypvw
Copy link

huypvw commented Dec 27, 2019

Hi, thanks, is very helpful. But it seem have a issue when object nested multi level. I think at line 20, it should become: objectToFormData(obj[property], fd, formKey);

@mencerz
Copy link

mencerz commented Feb 3, 2020

I'm little change previous class to fix of adding of Array with basic type's items like numbers to FormData. Also this one is good to use for convert the Vue.js Observer objects to the FormData

export default class Helpers {
    static convertModelToFormData(data = {}, form = null, namespace = '') {
        let files = {};
        let model = {};
        for (let propertyName in data) {
            if (data.hasOwnProperty(propertyName) && data[propertyName] instanceof File) {
                files[propertyName] = data[propertyName]
            } else {
                model[propertyName] = data[propertyName]
            }
        }

        model = JSON.parse(JSON.stringify(model))
        let formData = form || new FormData();

        for (let propertyName in model) {
            if (!model.hasOwnProperty(propertyName) || !model[propertyName]) continue;
            let formKey = namespace ? `${namespace}[${propertyName}]` : propertyName;
            if (model[propertyName] instanceof Date)
                formData.append(formKey, model[propertyName].toISOString());
            else if (model[propertyName] instanceof File) {
                formData.append(formKey, model[propertyName]);
            }
            else if (model[propertyName] instanceof Array) {
                model[propertyName].forEach((element, index) => {
                    const tempFormKey = `${formKey}[${index}]`;
                    if (typeof element === 'object') this.convertModelToFormData(element, formData, tempFormKey);
                    else formData.append(tempFormKey, element.toString());
                });
            }
            else if (typeof model[propertyName] === 'object' && !(model[propertyName] instanceof File))
                this.convertModelToFormData(model[propertyName], formData, formKey);
            else {
                formData.append(formKey, model[propertyName].toString());
            }
        }

        for (let propertyName in files) {
            if (files.hasOwnProperty(propertyName)) {
                formData.append(propertyName, files[propertyName]);
            }
        }
        return formData;
    }
}

There has bug, when property string and have value equal "" then this property not be exist in formData
data.my_input = '';
..
do this =) ...
..
formData.get('my_input') -> null

@yowee
Copy link

yowee commented Apr 11, 2020

You know what I did? I just sent the data as a String and I received it as a Json example

Client Side
let ageGroupTarget= { age: { minAge: this.myForm.get('minAge').value, maxAge: this.myForm.get('maxAge').value } }

formData.append('ageGroupTarget', JSON.stringify(ageGroupTarget));

server Side

JSON.parse(req.body.targetFollowers);

@bigabdoul
Copy link

bigabdoul commented Aug 5, 2020

Nice piece of code but it doesn't work for all scenarios. What about the following object?

{
  "id": 21,
  "displayName": "User",
  "email": "[email protected]",
  "firstName": "First name",
  "lastName": "Last name",
  "isActive": true,
  "createdBy": 52,
  "creationDate": "2018-07-23T11:45:15.77",
  "note": "",
  "profileId": 2,
  "userName": "username",
  "sendConfirmationEmail": false,
  "selectedRoles": [
    { "id": 7, "name": "Administrator", "selected": false },
    { "id": 1, "name": "Entry clerk", "selected": true },
    { "id": 2, "name": "Data entry supervisor", "selected": false }
  ],
  "moduleId": null,
  "competencies": [48, 44, 6],
  "originalCompetencies": [],
  "jobTitle": null,
  "departmentName": null,
  "selectedModuleId": 0,
  "fullName": "Full user name"
}

On the other hand, this code from an answer on SO does the job:

function buildFormData(formData, data, parentKey) {
  if (data && typeof data === 'object' && !(data instanceof Date) && !(data instanceof File) && !(data instanceof Blob)) {
    Object.keys(data).forEach(key => {
      buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key);
    });
  } else {
    const value = data == null ? '' : data;

    formData.append(parentKey, value);
  }
}

@rabbygit
Copy link

I have an array of object.In every single object, I have to pass image file for that object.

let myArray = [
{
"link" : "First link",
image : // Image file
},
{
"link" : "Second link",
image : // Image file
}
]

How can I append this array in formData ?

@bigabdoul
Copy link

I have an array of object.In every single object, I have to pass image file for that object.

let myArray = [
{
"link" : "First link",
image : // Image file
},
{
"link" : "Second link",
image : // Image file
}
]

How can I append this array in formData ?

Use the function function buildFormData(formData, data, parentKey) {...} like so:

const formData = new FormData();
buildFormData(formData, myArray);
// use myArray

@albertovincenzi
Copy link

Hi,
I figured out that the script removes all properties with null value. Why? Because typeof null is object so you get in the recursive loop and then null value will disappear. What do you think about checking obj[property] !== null ?

let fd = form || new FormData();
    let formKey;
    for (let property in obj) {
      if (obj.hasOwnProperty(property)) {
        if (namespace) {
          formKey = namespace + "[" + property + "]";
        } else {
          formKey = property;
        }
        if (
          typeof obj[property] === "object" &&
          !(obj[property] instanceof File) &&
          obj[property] !== null
        ) {
          this.objectToFormData(obj[property], fd, property);
        } else {
          if (obj[property] !== false) {
            fd.append(formKey, obj[property]);
          }
        }
      }
    }
    return fd;

@develforever
Copy link

Namespace is lost. This could fix it:
objectToFormData(obj[property], fd, namespace+"["+ property+"]");

@IvsonEmidio
Copy link

IvsonEmidio commented Aug 16, 2022

Update for Typescript 2022.
Edit: Date support removed.

function parseGenericObject<T>(
   object: T,
   form?: FormData,
   namespace?: string
 ): FormData {
   const formData = form || new FormData()
   for (const property in object) {
     const isPropertyExist = property in object

     if (!isPropertyExist) {
       continue
     }

     const contextProperty = object[property]
     const formKey = namespace ? `${namespace}[${property}]` : property

     if (
       typeof contextProperty === 'object' &&
       !(contextProperty instanceof File)
     ) {
       parseGenericObject<any>(contextProperty, formData, formKey)
     } else {
       formData.append(formKey, String(contextProperty))
     }
   }
   return formData
 }

@Ou7law007
Copy link

This does not work. It obviously is not nesting anything.

The server receives: {'p1[p1]' (all one key): val} instead of {'p1': {'p2': val}}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment