From 507c4cdf47d221b4240bfceb36384c76d6720913 Mon Sep 17 00:00:00 2001 From: dhaanisi Date: Sat, 14 Mar 2026 16:40:20 +0530 Subject: [PATCH 1/9] feat: add JSON to CSV converter --- public/locales/en/json.json | 6 + src/pages/tools/json/index.ts | 2 + src/pages/tools/json/json-to-csv/index.tsx | 181 ++++++++++++++++++++ src/pages/tools/json/json-to-csv/meta.ts | 15 ++ src/pages/tools/json/json-to-csv/service.ts | 114 ++++++++++++ src/pages/tools/json/json-to-csv/types.ts | 1 + 6 files changed, 319 insertions(+) create mode 100644 src/pages/tools/json/json-to-csv/index.tsx create mode 100644 src/pages/tools/json/json-to-csv/meta.ts create mode 100644 src/pages/tools/json/json-to-csv/service.ts create mode 100644 src/pages/tools/json/json-to-csv/types.ts diff --git a/public/locales/en/json.json b/public/locales/en/json.json index 75f59d0..399fdf8 100644 --- a/public/locales/en/json.json +++ b/public/locales/en/json.json @@ -51,6 +51,12 @@ "shortDescription": "Convert TSV to JSON format", "title": "TSV to JSON" }, + + "jsonToCsv": { + "description": "Convert JSON data to CSV format. Transform structured JSON objects into comma-separated values for spreadsheet applications.", + "shortDescription": "Convert JSON to CSV format", + "title": "JSON to CSV" + }, "validateJson": { "description": "Check if JSON is valid and well-formed.", "inputTitle": "Input JSON", diff --git a/src/pages/tools/json/index.ts b/src/pages/tools/json/index.ts index 9aba4f8..199b990 100644 --- a/src/pages/tools/json/index.ts +++ b/src/pages/tools/json/index.ts @@ -6,6 +6,7 @@ import { tool as jsonToXml } from './json-to-xml/meta'; import { tool as escapeJson } from './escape-json/meta'; import { tool as tsvToJson } from './tsv-to-json/meta'; import { tool as jsonComparison } from './json-comparison/meta'; +import { tool as jsonToCsv } from './json-to-csv/meta'; export const jsonTools = [ validateJson, @@ -13,6 +14,7 @@ export const jsonTools = [ jsonMinify, jsonStringify, jsonToXml, + jsonToCsv, escapeJson, tsvToJson, jsonComparison diff --git a/src/pages/tools/json/json-to-csv/index.tsx b/src/pages/tools/json/json-to-csv/index.tsx new file mode 100644 index 0000000..4970065 --- /dev/null +++ b/src/pages/tools/json/json-to-csv/index.tsx @@ -0,0 +1,181 @@ +import { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import ToolCodeInput from '@components/input/ToolCodeInput'; +import ToolTextResult from '@components/result/ToolTextResult'; +import { convertJsonToCsv } from './service'; +import { CardExampleType } from '@components/examples/ToolExamples'; +import { ToolComponentProps } from '@tools/defineTool'; +import { Box } from '@mui/material'; +import CheckboxWithDesc from '@components/options/CheckboxWithDesc'; +import SimpleRadio from '@components/options/SimpleRadio'; + +type InitialValuesType = { + delimiter: ',' | ';' | '\t'; + includeHeaders: boolean; + quoteStrings: 'always' | 'auto' | 'never'; +}; + +const initialValues: InitialValuesType = { + delimiter: ',', + includeHeaders: true, + quoteStrings: 'auto' +}; + +const exampleCards: CardExampleType[] = [ + { + title: 'Array of objects', + description: 'Convert a JSON array of objects into CSV rows.', + sampleText: ` +{ + "users": [ + { + "name": "John", + "age": 30, + "city": "New York" + }, + { + "name": "Alice", + "age": 25, + "city": "London" + } + ] +}`, + sampleResult: `name,age,city\nJohn,30,New York\nAlice,25,London`, + sampleOptions: { + ...initialValues + } + }, + { + title: 'Nested object (dot notation)', + description: + 'Nested keys are flattened using dot notation (e.g. address.city).', + sampleText: `[ + { + "name": "Bob", + "address": { + "city": "Berlin", + "zip": "10115" + } + } +]`, + sampleResult: `name,address.city,address.zip\nBob,Berlin,10115`, + sampleOptions: { + ...initialValues + } + } +]; + +export default function JsonToCsv({ title }: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + const compute = (values: InitialValuesType, input: string) => { + if (input) { + try { + const csvResult = convertJsonToCsv(input, values); + setResult(csvResult); + } catch (error) { + setResult( + `Error: ${ + error instanceof Error ? error.message : 'Invalid JSON format' + }` + ); + } + } + }; + + return ( + + } + resultComponent={ + + } + getGroups={({ values, updateField }) => [ + { + title: 'CSV Delimiter', + component: ( + + updateField('delimiter', ',')} + /> + updateField('delimiter', ';')} + /> + updateField('delimiter', '\t')} + /> + + ) + }, + { + title: 'String Quoting', + component: ( + + updateField('quoteStrings', 'auto')} + /> + updateField('quoteStrings', 'always')} + /> + updateField('quoteStrings', 'never')} + /> + + ) + }, + { + title: 'Headers', + component: ( + + updateField('includeHeaders', value)} + title="Include header row" + description="Output the JSON keys as the first row of the CSV." + /> + + ) + } + ]} + /> + ); +} diff --git a/src/pages/tools/json/json-to-csv/meta.ts b/src/pages/tools/json/json-to-csv/meta.ts new file mode 100644 index 0000000..e71d5db --- /dev/null +++ b/src/pages/tools/json/json-to-csv/meta.ts @@ -0,0 +1,15 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('json', { + path: 'json-to-csv', + icon: 'material-symbols:code', + keywords: [], + component: lazy(() => import('./index')), + i18n: { + name: 'json:jsonToCsv.title', + description: 'json:jsonToCsv.description', + shortDescription: 'json:jsonToCsv.shortDescription', + userTypes: ['generalUsers', 'developers'] + } +}); diff --git a/src/pages/tools/json/json-to-csv/service.ts b/src/pages/tools/json/json-to-csv/service.ts new file mode 100644 index 0000000..d69f6f0 --- /dev/null +++ b/src/pages/tools/json/json-to-csv/service.ts @@ -0,0 +1,114 @@ +type CsvOptions = { + delimiter: ',' | ';' | '\t'; + includeHeaders: boolean; + quoteStrings: 'always' | 'auto' | 'never'; +}; + +/** + * Flattens a JSON value into an array of row objects. + * Supports: + * - Array of objects → multiple rows + * - Single object → one row + * - Primitive → one row with a single "value" column + */ +function flattenToRows(json: unknown): Record[] { + if (Array.isArray(json)) { + return json.map((item, index) => { + if (item !== null && typeof item === 'object' && !Array.isArray(item)) { + return flattenObject(item as Record); + } + // Primitive inside array → index as key + return { [`item_${index}`]: String(item) }; + }); + } + + if (json !== null && typeof json === 'object') { + return [flattenObject(json as Record)]; + } + + // Top-level primitive + return [{ value: String(json) }]; +} + +/** + * Dot-notation flattening for nested objects. + * Arrays inside objects are serialised as JSON strings to keep CSV flat. + */ +function flattenObject( + obj: Record, + prefix = '' +): Record { + return Object.entries(obj).reduce>( + (acc, [key, val]) => { + const fullKey = prefix ? `${prefix}.${key}` : key; + + if (val !== null && typeof val === 'object' && !Array.isArray(val)) { + Object.assign( + acc, + flattenObject(val as Record, fullKey) + ); + } else { + acc[fullKey] = val === null || val === undefined ? '' : String(val); + } + + return acc; + }, + {} + ); +} + +function quoteCell( + value: string, + quoteStrings: CsvOptions['quoteStrings'], + delimiter: string +): string { + const needsQuoting = + value.includes(delimiter) || + value.includes('"') || + value.includes('\n') || + value.includes('\r'); + + const escaped = value.replace(/"/g, '""'); + + if (quoteStrings === 'always') return `"${escaped}"`; + if (quoteStrings === 'never') return value; + // 'auto' — only quote when necessary + return needsQuoting ? `"${escaped}"` : value; +} + +export function convertJsonToCsv(input: string, options: CsvOptions): string { + const { delimiter, includeHeaders, quoteStrings } = options; + + const parsed: unknown = JSON.parse(input); // throws on invalid JSON + const rows = flattenToRows(parsed); + + if (rows.length === 0) { + throw new Error('No data found in the provided JSON.'); + } + + // Collect all unique keys preserving insertion order + const headers = Array.from( + rows.reduce>((set, row) => { + Object.keys(row).forEach((k) => set.add(k)); + return set; + }, new Set()) + ); + + const lines: string[] = []; + + if (includeHeaders) { + lines.push( + headers.map((h) => quoteCell(h, quoteStrings, delimiter)).join(delimiter) + ); + } + + for (const row of rows) { + lines.push( + headers + .map((h) => quoteCell(row[h] ?? '', quoteStrings, delimiter)) + .join(delimiter) + ); + } + + return lines.join('\n'); +} diff --git a/src/pages/tools/json/json-to-csv/types.ts b/src/pages/tools/json/json-to-csv/types.ts new file mode 100644 index 0000000..2e634cc --- /dev/null +++ b/src/pages/tools/json/json-to-csv/types.ts @@ -0,0 +1 @@ +export type InitialValuesType = {}; From 7f1fe12cb7cc95b70629af7ded20e5dbdb57217e Mon Sep 17 00:00:00 2001 From: dhaanisi Date: Sat, 14 Mar 2026 19:05:31 +0530 Subject: [PATCH 2/9] test: add unit tests for json to csv converter --- .../tools/json/json-to-csv/service.test.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/pages/tools/json/json-to-csv/service.test.ts diff --git a/src/pages/tools/json/json-to-csv/service.test.ts b/src/pages/tools/json/json-to-csv/service.test.ts new file mode 100644 index 0000000..f7bd840 --- /dev/null +++ b/src/pages/tools/json/json-to-csv/service.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { convertJsonToCsv } from './service'; + +const defaultOptions: Parameters[1] = { + delimiter: ',', + includeHeaders: true, + quoteStrings: 'auto' +}; + +describe('convertJsonToCsv', () => { + it('converts simple JSON array to CSV', () => { + const input = JSON.stringify([ + { name: 'Dhee', age: 20 }, + { name: 'Jarvis', age: 999 } + ]); + + const result = convertJsonToCsv(input, defaultOptions); + + expect(result).toBe( + `name,age +Dhee,20 +Jarvis,999` + ); + }); + + it('handles a single object', () => { + const input = JSON.stringify({ + name: 'Dhee', + age: 20 + }); + + const result = convertJsonToCsv(input, defaultOptions); + + expect(result).toBe( + `name,age +Dhee,20` + ); + }); + + it('flattens nested objects', () => { + const input = JSON.stringify([ + { + name: 'Dhee', + info: { age: 20, city: 'Pune' } + } + ]); + + const result = convertJsonToCsv(input, defaultOptions); + + expect(result).toBe( + `name,info.age,info.city +Dhee,20,Pune` + ); + }); + + it('throws error for invalid JSON', () => { + const input = 'invalid json'; + + expect(() => convertJsonToCsv(input, defaultOptions)).toThrow(); + }); +}); From f6e1c13751b9c6a2498c123febd6051439aa74ca Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 15 Mar 2026 03:08:07 +0100 Subject: [PATCH 3/9] chore: getJsonHeaders as util function since reusable --- src/utils/json.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/utils/json.ts diff --git a/src/utils/json.ts b/src/utils/json.ts new file mode 100644 index 0000000..3065f08 --- /dev/null +++ b/src/utils/json.ts @@ -0,0 +1,18 @@ +/** + * Collects all unique keys from an array of row objects, preserving first-encountered order. + * Handles sparse rows where different rows may have different keys. + * + * @param rows - Array of flattened row objects + * @returns Array of unique header strings in insertion order + * + * @example + * getJsonHeaders([{ a: '1' }, { a: '2', b: '3' }]) // → ['a', 'b'] + */ +export function getJsonHeaders(rows: Record[]): string[] { + return Array.from( + rows.reduce>((set, row) => { + Object.keys(row).forEach((key) => set.add(key)); + return set; + }, new Set()) + ); +} From 8b046e93e3e347fed0279fb7d53e72ea32dc6e84 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 15 Mar 2026 03:11:53 +0100 Subject: [PATCH 4/9] chore: move types, remove unsafe quoting option, broaden delimiter type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move InitialValuesType to types.ts - Remove 'never' quote option — unsafe and produces invalid CSV when values contain double-quotes - Widen delimiter type from union to string to support custom delimiters --- src/pages/tools/json/json-to-csv/types.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/tools/json/json-to-csv/types.ts b/src/pages/tools/json/json-to-csv/types.ts index 2e634cc..87b223b 100644 --- a/src/pages/tools/json/json-to-csv/types.ts +++ b/src/pages/tools/json/json-to-csv/types.ts @@ -1 +1,5 @@ -export type InitialValuesType = {}; +export type InitialValuesType = { + delimiter: string; + includeHeaders: boolean; + quoteStrings: 'always' | 'auto'; +}; From 83989159109a70bec0e1774dd910f1f327295cc2 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 15 Mar 2026 03:20:10 +0100 Subject: [PATCH 5/9] - behaviour slightly change03:19chore: refactor service internals - Flatten JSON using recursive traversal instead of iterative approach - Extract header collection to shared util - Import InitialValuesType from types.ts - Replace bare primitive fallback with explicit error throw - Use CRLF line endings to comply with RFC 4180 --- src/pages/tools/json/json-to-csv/service.ts | 156 ++++++++++---------- 1 file changed, 80 insertions(+), 76 deletions(-) diff --git a/src/pages/tools/json/json-to-csv/service.ts b/src/pages/tools/json/json-to-csv/service.ts index d69f6f0..13cd6e2 100644 --- a/src/pages/tools/json/json-to-csv/service.ts +++ b/src/pages/tools/json/json-to-csv/service.ts @@ -1,114 +1,118 @@ -type CsvOptions = { - delimiter: ',' | ';' | '\t'; - includeHeaders: boolean; - quoteStrings: 'always' | 'auto' | 'never'; -}; +import { InitialValuesType } from './types'; +import { getJsonHeaders } from 'utils/json'; /** - * Flattens a JSON value into an array of row objects. - * Supports: - * - Array of objects → multiple rows - * - Single object → one row - * - Primitive → one row with a single "value" column + * Recursively flattens any JSON value into a flat object. + * Objects → dot notation + * Arrays → index notation + */ +function flattenRecursive( + value: unknown, + prefix = '', + result: Record = {} +): Record { + if (value === null || value === undefined) { + if (prefix) result[prefix] = ''; + return result; + } + + if (typeof value !== 'object') { + if (prefix) result[prefix] = String(value); + return result; + } + + if (Array.isArray(value)) { + value.forEach((item, index) => { + const newKey = prefix ? `${prefix}[${index}]` : `[${index}]`; + flattenRecursive(item, newKey, result); + }); + return result; + } + + for (const [key, val] of Object.entries(value)) { + const newKey = prefix ? `${prefix}.${key}` : key; + flattenRecursive(val, newKey, result); + } + + return result; +} + +/** + * Converts any JSON structure into row objects. */ function flattenToRows(json: unknown): Record[] { if (Array.isArray(json)) { - return json.map((item, index) => { - if (item !== null && typeof item === 'object' && !Array.isArray(item)) { - return flattenObject(item as Record); - } - // Primitive inside array → index as key - return { [`item_${index}`]: String(item) }; - }); + return json.map((item) => flattenRecursive(item)); } - if (json !== null && typeof json === 'object') { - return [flattenObject(json as Record)]; + if (typeof json === 'object' && json !== null) { + return [flattenRecursive(json)]; } - // Top-level primitive - return [{ value: String(json) }]; -} - -/** - * Dot-notation flattening for nested objects. - * Arrays inside objects are serialised as JSON strings to keep CSV flat. - */ -function flattenObject( - obj: Record, - prefix = '' -): Record { - return Object.entries(obj).reduce>( - (acc, [key, val]) => { - const fullKey = prefix ? `${prefix}.${key}` : key; - - if (val !== null && typeof val === 'object' && !Array.isArray(val)) { - Object.assign( - acc, - flattenObject(val as Record, fullKey) - ); - } else { - acc[fullKey] = val === null || val === undefined ? '' : String(val); - } - - return acc; - }, - {} + throw new Error( + 'JSON input must be an object or array of objects, not a bare primitive.' ); } -function quoteCell( - value: string, - quoteStrings: CsvOptions['quoteStrings'], - delimiter: string -): string { +/** + * Escapes and quotes CSV cells according to options + */ +function quoteCell(value: string, options: InitialValuesType): string { + const { delimiter, quoteStrings } = options; + + const escaped = value.replace(/"/g, '""'); + const needsQuoting = value.includes(delimiter) || value.includes('"') || value.includes('\n') || value.includes('\r'); - const escaped = value.replace(/"/g, '""'); - if (quoteStrings === 'always') return `"${escaped}"`; - if (quoteStrings === 'never') return value; - // 'auto' — only quote when necessary + return needsQuoting ? `"${escaped}"` : value; } -export function convertJsonToCsv(input: string, options: CsvOptions): string { - const { delimiter, includeHeaders, quoteStrings } = options; +/** + * Converts JSON string to CSV + */ +export function convertJsonToCsv( + input: string, + options: InitialValuesType +): string { + const { delimiter, includeHeaders } = options; - const parsed: unknown = JSON.parse(input); // throws on invalid JSON - const rows = flattenToRows(parsed); + let parsed: unknown; + + try { + parsed = JSON.parse(input); + } catch { + throw new Error('Invalid JSON input.'); + } + + const rows = flattenToRows(parsed).filter( + (row) => Object.keys(row).length > 0 + ); if (rows.length === 0) { throw new Error('No data found in the provided JSON.'); } - // Collect all unique keys preserving insertion order - const headers = Array.from( - rows.reduce>((set, row) => { - Object.keys(row).forEach((k) => set.add(k)); - return set; - }, new Set()) - ); + const headers = getJsonHeaders(rows); const lines: string[] = []; if (includeHeaders) { - lines.push( - headers.map((h) => quoteCell(h, quoteStrings, delimiter)).join(delimiter) - ); + lines.push(headers.map((h) => quoteCell(h, options)).join(delimiter)); } for (const row of rows) { - lines.push( - headers - .map((h) => quoteCell(row[h] ?? '', quoteStrings, delimiter)) - .join(delimiter) - ); + const line = headers + .map((header) => quoteCell(row[header] ?? '', options)) + .join(delimiter); + + lines.push(line); } - return lines.join('\n'); + return lines.join('\r\n'); } From bd2ee9093c777d314ed218243f9193313fa2e86c Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 15 Mar 2026 03:20:41 +0100 Subject: [PATCH 6/9] chore: rewritten testcase file since behaviour updated --- .../tools/json/json-to-csv/service.test.ts | 187 ++++++++++++++---- 1 file changed, 149 insertions(+), 38 deletions(-) diff --git a/src/pages/tools/json/json-to-csv/service.test.ts b/src/pages/tools/json/json-to-csv/service.test.ts index f7bd840..f6a2011 100644 --- a/src/pages/tools/json/json-to-csv/service.test.ts +++ b/src/pages/tools/json/json-to-csv/service.test.ts @@ -8,54 +8,165 @@ const defaultOptions: Parameters[1] = { }; describe('convertJsonToCsv', () => { - it('converts simple JSON array to CSV', () => { - const input = JSON.stringify([ - { name: 'Dhee', age: 20 }, - { name: 'Jarvis', age: 999 } - ]); + describe('basic conversion', () => { + it('converts a flat array of objects', () => { + const input = JSON.stringify([ + { name: 'Alice', age: 30 }, + { name: 'Bob', age: 25 } + ]); - const result = convertJsonToCsv(input, defaultOptions); - - expect(result).toBe( - `name,age -Dhee,20 -Jarvis,999` - ); - }); - - it('handles a single object', () => { - const input = JSON.stringify({ - name: 'Dhee', - age: 20 + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `name,age\r\nAlice,30\r\nBob,25` + ); }); - const result = convertJsonToCsv(input, defaultOptions); + it('converts a single object', () => { + const input = JSON.stringify({ name: 'Alice', age: 30 }); - expect(result).toBe( - `name,age -Dhee,20` - ); + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `name,age\r\nAlice,30` + ); + }); + + it('excludes header row when includeHeaders is false', () => { + const input = JSON.stringify([{ name: 'Alice', age: 30 }]); + + expect( + convertJsonToCsv(input, { ...defaultOptions, includeHeaders: false }) + ).toBe(`Alice,30`); + }); }); - it('flattens nested objects', () => { - const input = JSON.stringify([ - { - name: 'Dhee', - info: { age: 20, city: 'Pune' } - } - ]); + describe('flattening', () => { + it('flattens nested objects using dot notation', () => { + const input = JSON.stringify([ + { name: 'Alice', address: { city: 'Paris', zip: '75000' } } + ]); - const result = convertJsonToCsv(input, defaultOptions); + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `name,address.city,address.zip\r\nAlice,Paris,75000` + ); + }); - expect(result).toBe( - `name,info.age,info.city -Dhee,20,Pune` - ); + it('flattens arrays using index notation', () => { + const input = JSON.stringify([ + { name: 'Alice', tags: ['admin', 'user'] } + ]); + + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `name,tags[0],tags[1]\r\nAlice,admin,user` + ); + }); + + it('flattens deeply nested structures', () => { + const input = JSON.stringify([ + { user: { address: { geo: { lat: 48.8566, lng: 2.3522 } } } } + ]); + + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `user.address.geo.lat,user.address.geo.lng\r\n48.8566,2.3522` + ); + }); }); - it('throws error for invalid JSON', () => { - const input = 'invalid json'; + describe('sparse rows', () => { + it('fills missing keys with empty values', () => { + const input = JSON.stringify([ + { name: 'Alice', age: 30 }, + { name: 'Bob', city: 'Paris' } + ]); - expect(() => convertJsonToCsv(input, defaultOptions)).toThrow(); + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `name,age,city\r\nAlice,30,\r\nBob,,Paris` + ); + }); + + it('filters out empty objects', () => { + const input = JSON.stringify([{}, { name: 'Alice' }]); + + expect(convertJsonToCsv(input, defaultOptions)).toBe(`name\r\nAlice`); + }); + }); + + describe('quoting', () => { + it('quotes cells containing the delimiter', () => { + const input = JSON.stringify([{ name: 'Smith, John' }]); + + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `name\r\n"Smith, John"` + ); + }); + + it('escapes double-quotes by doubling them', () => { + const input = JSON.stringify([{ name: 'He said "hello"' }]); + + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `name\r\n"He said ""hello"""` + ); + }); + + it('quotes all cells when quoteStrings is always', () => { + const input = JSON.stringify([{ name: 'Alice', age: 30 }]); + + expect( + convertJsonToCsv(input, { ...defaultOptions, quoteStrings: 'always' }) + ).toBe(`"name","age"\r\n"Alice","30"`); + }); + + it('quotes cells containing newlines', () => { + const input = JSON.stringify([{ notes: 'line1\nline2' }]); + + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `notes\r\n"line1\nline2"` + ); + }); + }); + + describe('delimiters', () => { + it('uses semicolon as delimiter', () => { + const input = JSON.stringify([{ name: 'Alice', age: 30 }]); + + expect( + convertJsonToCsv(input, { ...defaultOptions, delimiter: ';' }) + ).toBe(`name;age\r\nAlice;30`); + }); + + it('uses tab as delimiter', () => { + const input = JSON.stringify([{ name: 'Alice', age: 30 }]); + + expect( + convertJsonToCsv(input, { ...defaultOptions, delimiter: '\t' }) + ).toBe(`name\tage\r\nAlice\t30`); + }); + }); + + describe('null and undefined values', () => { + it('converts null values to empty strings', () => { + const input = JSON.stringify([{ name: 'Alice', age: null }]); + + expect(convertJsonToCsv(input, defaultOptions)).toBe( + `name,age\r\nAlice,` + ); + }); + }); + + describe('errors', () => { + it('throws on invalid JSON', () => { + expect(() => convertJsonToCsv('invalid json', defaultOptions)).toThrow( + 'Invalid JSON input.' + ); + }); + + it('throws on bare primitive', () => { + expect(() => convertJsonToCsv('42', defaultOptions)).toThrow( + 'JSON input must be an object or array of objects, not a bare primitive.' + ); + }); + + it('throws when no data rows are found', () => { + expect(() => + convertJsonToCsv(JSON.stringify([{}, {}]), defaultOptions) + ).toThrow('No data found in the provided JSON.'); + }); }); }); From 9005c1c38b604a57b06657ff2a1749d406bd9b77 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 15 Mar 2026 03:21:15 +0100 Subject: [PATCH 7/9] chore: locales updated for full translation support --- public/locales/en/json.json | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/public/locales/en/json.json b/public/locales/en/json.json index 399fdf8..1ecbc04 100644 --- a/public/locales/en/json.json +++ b/public/locales/en/json.json @@ -53,9 +53,29 @@ }, "jsonToCsv": { - "description": "Convert JSON data to CSV format. Transform structured JSON objects into comma-separated values for spreadsheet applications.", + "description": "Convert JSON data to CSV format. Supports nested objects and arrays, automatic header detection, and configurable delimiters and quoting — ready for Excel, Google Sheets, or any spreadsheet application.", "shortDescription": "Convert JSON to CSV format", - "title": "JSON to CSV" + "title": "JSON to CSV", + "quotingOption": "String Quoting", + "delimiterOption": "Delimiter", + "headerOption": "Headers", + "inputTitle": "Input JSON", + "outputTitle": "Output CSV", + "options": { + "delimiter": "Character used to separate values in each row (e.g. comma, semicolon, tab)", + "alwaysQuote": { + "label": "Always quote", + "description": "Wrap every cell in double-quotes, regardless of its content." + }, + "autoQuote": { + "label": "Auto (recommended)", + "description": "Only quote cells that contain the delimiter, a double-quote, or a newline. Keeps output clean and minimal." + }, + "header": { + "label": "Include header row", + "description": "Use JSON keys as column names in the first row. Recommended for readability and import compatibility." + } + } }, "validateJson": { "description": "Check if JSON is valid and well-formed.", From d6d33c799d2afac810c05023cc753c4190c00b37 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 15 Mar 2026 03:23:18 +0100 Subject: [PATCH 8/9] chore: improve configurability and content - Allow custom delimiter input beyond preset options - Add full i18n translation support - Fix example cards data and titles --- src/pages/tools/json/json-to-csv/index.tsx | 124 +++++++++------------ 1 file changed, 54 insertions(+), 70 deletions(-) diff --git a/src/pages/tools/json/json-to-csv/index.tsx b/src/pages/tools/json/json-to-csv/index.tsx index 4970065..0ac36ed 100644 --- a/src/pages/tools/json/json-to-csv/index.tsx +++ b/src/pages/tools/json/json-to-csv/index.tsx @@ -7,13 +7,10 @@ import { CardExampleType } from '@components/examples/ToolExamples'; import { ToolComponentProps } from '@tools/defineTool'; import { Box } from '@mui/material'; import CheckboxWithDesc from '@components/options/CheckboxWithDesc'; +import TextFieldWithDesc from '@components/options/TextFieldWithDesc'; import SimpleRadio from '@components/options/SimpleRadio'; - -type InitialValuesType = { - delimiter: ',' | ';' | '\t'; - includeHeaders: boolean; - quoteStrings: 'always' | 'auto' | 'never'; -}; +import { InitialValuesType } from './types'; +import { useTranslation } from 'react-i18next'; const initialValues: InitialValuesType = { delimiter: ',', @@ -24,23 +21,14 @@ const initialValues: InitialValuesType = { const exampleCards: CardExampleType[] = [ { title: 'Array of objects', - description: 'Convert a JSON array of objects into CSV rows.', - sampleText: ` -{ - "users": [ - { - "name": "John", - "age": 30, - "city": "New York" - }, - { - "name": "Alice", - "age": 25, - "city": "London" - } - ] -}`, - sampleResult: `name,age,city\nJohn,30,New York\nAlice,25,London`, + description: + 'Convert multiple JSON objects into CSV rows, one row per object.', + sampleText: `[ + { "name": "John Doe", "age": 25, "city": "New York" }, + { "name": "Jane Doe", "age": 30, "city": "Los Angeles" }, + { "name": "Bob Smith", "age": 22, "city": "Chicago" } +]`, + sampleResult: `name,age,city\nJohn Doe,25,New York\nJane Doe,30,Los Angeles\nBob Smith,22,Chicago`, sampleOptions: { ...initialValues } @@ -51,14 +39,32 @@ const exampleCards: CardExampleType[] = [ 'Nested keys are flattened using dot notation (e.g. address.city).', sampleText: `[ { - "name": "Bob", + "name": "John Doe", + "age": 25, "address": { - "city": "Berlin", - "zip": "10115" - } + "street": "123 Main St", + "city": "New York", + "state": "NY", + "postalCode": "10001" + }, + "hobbies": ["reading", "running"] } ]`, - sampleResult: `name,address.city,address.zip\nBob,Berlin,10115`, + sampleResult: `name,age,address.street,address.city,address.state,address.postalCode,hobbies[0],hobbies[1]\nJohn Doe,25,123 Main St,New York,NY,10001,reading,running`, + sampleOptions: { + ...initialValues + } + }, + { + title: 'Sparse rows', + description: + 'Missing keys are filled with empty values to keep columns aligned.', + sampleText: `[ + { "name": "Alice", "age": 30 }, + { "name": "Bob", "city": "Paris" }, + { "name": "Carol", "age": 25, "city": "Rome" } +]`, + sampleResult: `name,age,city\nAlice,30,\nBob,,Paris\nCarol,25,Rome`, sampleOptions: { ...initialValues } @@ -66,6 +72,7 @@ const exampleCards: CardExampleType[] = [ ]; export default function JsonToCsv({ title }: ToolComponentProps) { + const { t } = useTranslation('json'); const [input, setInput] = useState(''); const [result, setResult] = useState(''); @@ -94,83 +101,60 @@ export default function JsonToCsv({ title }: ToolComponentProps) { exampleCards={exampleCards} inputComponent={ } resultComponent={ - + } getGroups={({ values, updateField }) => [ { - title: 'CSV Delimiter', + title: t('jsonToCsv.delimiterOption'), component: ( - updateField('delimiter', ',')} - /> - updateField('delimiter', ';')} - /> - updateField('delimiter', '\t')} + updateField('delimiter', val)} /> ) }, { - title: 'String Quoting', + title: t('jsonToCsv.quotingOption'), component: ( updateField('quoteStrings', 'auto')} /> updateField('quoteStrings', 'always')} /> - updateField('quoteStrings', 'never')} - /> ) }, { - title: 'Headers', + title: t('jsonToCsv.headerOption'), component: ( updateField('includeHeaders', value)} - title="Include header row" - description="Output the JSON keys as the first row of the CSV." + title={t('jsonToCsv.options.header.label')} + description={t('jsonToCsv.options.header.description')} /> ) From 64e40d7c87aca8437b61de3c68a65c253b6b5d8e Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 15 Mar 2026 03:41:27 +0100 Subject: [PATCH 9/9] chore: error thrown on delimiter missing --- src/pages/tools/json/json-to-csv/service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/tools/json/json-to-csv/service.ts b/src/pages/tools/json/json-to-csv/service.ts index 13cd6e2..33ddeb4 100644 --- a/src/pages/tools/json/json-to-csv/service.ts +++ b/src/pages/tools/json/json-to-csv/service.ts @@ -82,6 +82,8 @@ export function convertJsonToCsv( ): string { const { delimiter, includeHeaders } = options; + if (!delimiter) throw new Error('No CSV delimiter.'); + let parsed: unknown; try {