Docexp

JSON to CSV: What Happens to Nested Objects, Arrays and Missing Keys

An API hands you JSON. The place you have to put the data — Excel, Google Sheets, a bulk-upload form — wants CSV. The conversion sounds mechanical, and for a flat, uniform array of objects it is. It stops being mechanical the moment one record has a nested object, one has an array as a value, or one record simply doesn't carry a field the others do. Those three cases account for almost every "why is my converted file wrong" question, and each one has a specific, predictable cause.

CSV is a table. JSON is a tree. Conversion is a guess about how to flatten it

A CSV file has exactly two dimensions: rows and columns, every cell a plain string. JSON has no such limit — a value can be a string, a number, an object with its own nested objects, or an array of any of those, arbitrarily deep. Converting JSON to CSV means picking one flat shape out of a structure that doesn't have to be flat, and every quirk below is really the same question answered differently: what do you do with a value that isn't a plain string or number?

Nested objects become the text "[object Object]"

This is the one that looks most like a bug and isn't. Say one record in your array looks like this:

{ "name": "Ada", "address": { "city": "Pune", "zip": "411001" } }

address isn't a string — it's a whole object. A converter that just writes out each value as text has to call something equivalent to JavaScript's String() on it, and String() on a plain object doesn't inspect its contents. It returns the literal characters [object Object], because that's what Object.prototype.toString produces when nothing more specific is defined. The city and zip aren't dropped so much as replaced with a placeholder that looks like an error message but technically isn't one — the conversion did exactly what it was told.

The fix has to happen before conversion, not after. Flatten the nested object into its own top-level keys — address.city and address.zip rather than address — so each one is a plain string the converter can actually place in a cell. Docexp's JSON to CSV converter only handles flat objects for this reason: there's no single "correct" way to flatten an arbitrarily nested structure (dot notation? underscores? one column per leaf, or the whole subtree as a JSON string?), so guessing wrong silently is worse than requiring flat input and being honest that nesting needs a decision a human should make.

Arrays as values become one comma-joined cell, not several columns

This one is different from the object case, and worth knowing precisely because the two nearly identical-looking problems have different outcomes. A record like:

{ "name": "Ada", "tags": ["engineer", "mathematician"] }

tags is an array, not a plain object, and a JavaScript array's String() conversion isn't [object Object] — arrays stringify by joining their elements with commas. So tags becomes the text engineer,mathematician in a single cell. Because that text itself contains a comma, a properly written CSV converter has to quote the whole cell ("engineer,mathematician") so it isn't misread as two columns when the file is opened. Docexp's converter quotes any cell containing a comma or line break, so the two tags stay together as one field rather than spilling into whatever column comes next.

It's a usable fallback for a short list of primitive values. It stops being usable the moment the array holds objects rather than strings — an array of order line items, say — because each element then stringifies to [object Object] too, joined by commas into [object Object],[object Object]. An array of objects needs its own row per element, which is a restructuring decision, not something a converter should attempt silently.

Records with different keys: the column that goes missing, not the row that shifts

This is the failure mode that causes real, hard-to-spot damage, because unlike the two above nothing in the output looks obviously wrong — it just quietly loses a field for some rows and not others.

Say your API returns:

[
  { "name": "Ada", "email": "ada@example.com" },
  { "name": "Grace", "email": "grace@example.com", "phone": "+1-555-0100" },
  { "name": "Alan", "email": "alan@example.com" }
]

Grace's record has a phone field the other two don't. A converter that reads only the first object's keys to decide the header row would emit name,email — and then Grace's phone number either gets silently dropped, or worse, gets tacked onto the end of her row where a naive column-count mismatch shifts everything in every tool that reads the file back.

The correct behaviour is to scan every object in the array first, build the header from every key seen anywhere — in the order each key is first encountered — and give any record missing a key an empty cell rather than a shifted one. For the example above that's a header of name,email,phone, with Ada's and Alan's phone cells simply blank. Docexp's converter does exactly this: nothing is dropped, and no row ever shifts out of alignment with its header just because an earlier record happened to be missing a field a later one has.

null, missing keys, and false all become an empty or literal string — not nothing

Three values that look similar in JSON land differently in a CSV cell:

  • A key that's missing entirely from a record becomes an empty cell — there's nothing to write.
  • A key present with the value null also becomes an empty cell, for the same reason: there's no meaningful string for "null."
  • A key with the value false or 0 is not empty — it becomes the literal text false or 0. These are real values, not absences, and a converter that treats a falsy value as "nothing to write" would wrongly erase every record where a boolean flag happens to be false or a count happens to be zero.

That distinction matters most for boolean and numeric fields — a "verified" or "in_stock" column where false is a real, common answer that has to survive the round trip as visibly as true does.

Converting without any of this catching you out

  1. If any record has a nested object as a value, flatten it into its own keys first — address.city rather than address — since Docexp's JSON to CSV converter works on flat objects only and will otherwise write [object Object] for that field.
  2. Paste the JSON array in. It has to be an array — a single object wrapped in { } isn't a table with rows, so wrap a lone object in [ ] first if that's what you have.
  3. Convert and check the header row against the widest record you expect — if a field you know exists on some records isn't there, the array itself is missing it, not the converter.

Nothing here is uploaded to convert it: the array is parsed and turned into CSV entirely in your browser.

Going the other way: CSV to JSON keeps everything as text, on purpose

Docexp's CSV to JSON converter takes the first row as headers and turns each following row into one object — and deliberately does not try to guess that a column of digits should become a JSON number. A CSV column of postcodes or reference IDs that happen to look numeric would otherwise have its own leading-zero problem in reverse: guess wrong and 01234 becomes 1234, or a long numeric ID gets rounded the way a spreadsheet rounds a float. Keeping every value as a string keeps exactly what was in the file, which is the safer default when the converter has no way to know which columns are meant to be numbers and which only look like them.

Frequently asked questions

Why does my converted CSV say "[object Object]" instead of my data?

That field's value was itself a JSON object rather than a plain string or number — an address field holding { city, zip }, for example. String() on a plain object returns the literal text [object Object] rather than its contents. Flatten the nested object into its own top-level keys (address.city, address.zip) before converting.

Why is one column empty for some rows and not others?

Because not every object in your array has that key. Docexp's converter builds the header from every key seen across the whole array, so a record missing a field gets a blank cell in that column rather than having the field dropped or the rest of the row shifted out of place.

Can I convert a single JSON object instead of an array?

No — a CSV is a table of rows, so the converter needs a non-empty array. Wrap a lone object in square brackets ([ { ... } ]) and it converts as a one-row table.

What happens to an array value, like a list of tags?

It's joined into one comma-separated string in a single cell (and quoted, since the cell itself now contains a comma) — usable for a short list of plain values, but an array of objects will stringify each element to [object Object] the same way a single nested object does.


The JSON to CSV and CSV to JSON converters both run entirely in your browser. Your data is parsed and converted on your own device — nothing is uploaded to a server at any point.