Converting JSON CSV XML - SmithSoft-Studios/JAZMIN GitHub Wiki

Converting JSON, CSV and XML

Every JAZMIN file converts to JSON, CSV and XML and back in one call, with a filter and a column list if you want only part of it. Large files convert as streams, at any size.

Direction JavaScript .NET
JSON → JAZMIN fromJSON(text, target, options) JazminConvert.FromJson(json, settings)
JSON file of any size → JAZMIN importJSONFile(path, target, options) JazminConvert.FromJsonFile(path, output, settings)
JAZMIN → JSON toJSON(reader, { filter, select, pretty, omitNulls }) JazminConvert.ToJson(bytes, settings, filter)
CSV → JAZMIN fromCSV(text, target, { inferTypes, delimiter }) JazminConvert.FromCsv(csv, settings, inferTypes)
CSV file of any size → JAZMIN importCSVFile(path, target, { inferTypes, delimiter, columns }) JazminConvert.FromCsvFile(path, output, settings, inferTypes, delimiter, columns)
JAZMIN → CSV toCSV(reader, { filter, select, delimiter }) JazminConvert.ToCsv(bytes, settings, filter)
XML → JAZMIN fromXML(text, target, { inferTypes }) JazminConvert.FromXml(xml, settings)
XML file of any size → JAZMIN importXMLFile(path, target, { inferTypes, columns }) JazminConvert.FromXmlFile(path, output, settings, inferTypes, columns)
JAZMIN → XML toXML(reader, options) JazminConvert.ToXml(bytes, settings, filter)
Stream to a file exportFile(reader, 'json' | 'csv' | 'xml', path, options) JsonFormat.Write, CsvFormat.Write, XmlFormat.Write
Rows as a JSON stream or tokens JazminJsonStream, JazminJsonReader
Nested JSON and XML { shape } on toJSON, toXML, exportFile JazminShape (Export Shapes)

Pass null as the target in JavaScript to get the bytes back instead of writing a file.

The rules, shown

import { fromCSV, fromXML, open, toCSV, toJSON, toXML } from '@smithsoft-studios/jazmin';

const csv = [
  'code,name,joined,balance,note',
  '0042,Ann,2024-01-02T00:00:00Z,1520.75,""', // "" is an empty string
  '0107,Bob,2024-03-04T00:00:00Z,99.1,',      // nothing is null
].join('\n');

const inferred = open(fromCSV(csv, null));
console.log('inferred:', inferred.columns.map((c) => `${c.name} ${c.type}`).join(', '));
console.log(inferred.get(0));

// Codes with leading zeros: turn inference off (every column a string), or declare the columns
const asText = open(fromCSV(csv, null, { inferTypes: false }));
console.log('as text: ', asText.get(0).code, typeof asText.get(0).code);

// Out again: JSON keeps exact values and ISO dates; CSV keeps null and "" apart; XML leaves nulls out
console.log(toJSON(inferred, { select: ['code', 'joined', 'note'] }));
console.log(toCSV(inferred));
console.log(toXML(inferred, { filter: { name: 'Bob' }, select: ['name', 'note'] }));

// XML in: the canonical shape <jazmin><row><column>value</column></row></jazmin>
const fromXml = open(fromXML('<jazmin><row><id>1</id><city>Cape Town</city></row><row><id>2</id></row></jazmin>', null));
console.log(fromXml.columns.map((c) => `${c.name} ${c.type}${c.nullable ? '?' : ''}`).join(', '), [...fromXml.rows()]);
inferred: code int, name string, joined string, balance float, note string
{
  code: 42,
  name: 'Ann',
  joined: '2024-01-02T00:00:00Z',
  balance: 1520.75,
  note: ''
}
as text:  0042 string
[{"code":42,"joined":"2024-01-02T00:00:00Z","note":""},{"code":107,"joined":"2024-03-04T00:00:00Z","note":null}]
code,name,joined,balance,note
42,Ann,2024-01-02T00:00:00Z,1520.75,""
107,Bob,2024-03-04T00:00:00Z,99.1,

<?xml version="1.0" encoding="UTF-8"?>
<jazmin>
  <row><name>Bob</name></row>
</jazmin>

id int, city string? [ { id: 1, city: 'Cape Town' }, { id: 2, city: null } ]
  • JSON: large integers and decimals are written exactly, even beyond JavaScript's 2^53. Dates use ISO-8601 UTC, and binary data is base64. omitNulls: true leaves null properties out; pretty: true indents.
  • CSV: an empty field means null, while "" means an empty string, so round trips keep the difference. Types are inferred unless you set inferTypes: false; turn inference off (or declare columns) for codes with leading zeros.
  • XML: <jazmin><row><column>value</column></row></jazmin>. Null values are left out. Column names that aren't valid XML names are written as <field name="...">. A column that first appears in a later row is nullable.
  • Dates in text stay strings when types are inferred, as joined did above. Declare the column as datetime (with columns) to store real dates; for JSON, detectDates: true also works.

Files of any size

importJSONFile, importCSVFile and importXMLFile (.NET FromJsonFile, FromCsvFile, FromXmlFile) read a file a block at a time, so its size doesn't matter. They read it twice (first to work out the column types, then to write the rows) unless you pass columns; then every header name or element must be one of those columns, and the file is read once.

import fs from 'node:fs';
import { exportFile, importCSVFile, open } from '@smithsoft-studios/jazmin';

const lines = ['account,at,amount'];
for (let i = 0; i < 100_000; i++) lines.push(`ACC-${i % 250},2026-01-01T00:${String(i % 60).padStart(2, '0')}:00Z,${(i % 1000) / 4}`);
fs.writeFileSync('transactions.csv', lines.join('\n'));

// Declared columns: the file is read once, and 'at' is stored as a real date (inference would leave it text)
importCSVFile('transactions.csv', 'transactions.jzm', {
  columns: [
    { name: 'account', type: 'string', index: 'sorted' },
    { name: 'at', type: 'datetime' },
    { name: 'amount', type: 'float' },
  ],
});
const reader = open('transactions.jzm');
console.log(reader.rowCount, 'rows:', reader.columns.map((c) => `${c.name} ${c.type}`).join(', '));
console.log('CSV', Math.round(fs.statSync('transactions.csv').size / 1024), 'KB -> JAZMIN', Math.round(fs.statSync('transactions.jzm').size / 1024), 'KB');

// Out again, streamed to disk: one account, as CSV
exportFile(reader, 'csv', 'acc-7.csv', { filter: { account: 'ACC-7' } });
console.log(fs.readFileSync('acc-7.csv', 'utf8').split('\n').slice(0, 3).join(' | '));

// Back in, with the columns known: read once, each value as its column's type
importCSVFile('acc-7.csv', 'acc-7.jzm', { columns: reader.columns });
console.log(open('acc-7.jzm').rowCount, 'rows re-imported');
100000 rows: account string, at datetime, amount float
CSV 3332 KB -> JAZMIN 94 KB
account,at,amount
 | ACC-7,2026-01-01T00:07:00.000Z,1.75
 | ACC-7,2026-01-01T00:17:00.000Z,64.25
400 rows re-imported

A 1 GB file:

Library CSV (10 million rows) XML (4.6 million rows)
.NET 14 s, 63 MB 11 s, 65 MB
Node, with --max-old-space-size=64 --max-semi-space-size=2 44 s, 92 MB 48 s, 86 MB
Node, without those flags (garbage collected late) 32 s, 316 MB 41 s, 250 MB

fromCSV and fromXML hold the text and then every row as an object: a 200 MB file took 1.6 GB (CSV) and 1.3 GB (XML) that way. Use the import...File functions for anything large.

In .NET

using Jazmin;
using Jazmin.Formats;
using Jazmin.Query;
using Jazmin.Serialization;

byte[] bytes = JazminConvert.FromCsv("""
    id,name,balance
    1,Ann,1520.75
    2,Bob,
    """);
Console.WriteLine(JazminConvert.ToJson(bytes));
Console.WriteLine(JazminConvert.ToJson(bytes, new JazminSerializerSettings { NullValueHandling = NullValueHandling.Ignore }, JazminFilter.Eq("id", 2)));
Console.WriteLine(JazminConvert.ToXml(bytes, filter: JazminFilter.Eq("id", 1)));

// Tokens, one at a time, as Newtonsoft's JsonReader gives them: no JSON text is written or parsed
using var reader = JazminReader.Open(bytes);
using var json = new JazminJsonReader(reader, JazminFilter.Eq("id", 1));
while (json.Read())
    if (json.TokenType == JazminJsonToken.PropertyName) Console.Write($"{json.Path} = ");
    else if (json.Value is not null) Console.WriteLine($"{json.Value} ({json.TokenType})");
[{"id":1,"name":"Ann","balance":1520.75},{"id":2,"name":"Bob","balance":null}]
[{"id":2,"name":"Bob"}]
<?xml version="1.0" encoding="utf-8"?>
<jazmin>
  <row>
    <id>1</id>
    <name>Ann</name>
    <balance>1520.75</balance>
  </row>
</jazmin>
[0].id = 1 (Integer)
[0].name = Ann (String)
[0].balance = 1520.75 (Float)

A query's rows as a JSON stream, without building a string: for an HTTP response or any System.Text.Json pipeline.

using var stream = new JazminJsonStream(reader, JazminFilter.Eq("account", "ACC-100001"), new() { Select = ["at", "amount"] });
await foreach (var line in JsonSerializer.DeserializeAsyncEnumerable<StatementLine>(stream)) { /* ... */ }
// ASP.NET Core: return Results.Stream(new JazminJsonStream(reader), "application/json");
  • The JSON is ToJson's: an array of row objects, the same bytes. JazminJsonStream takes Formatting and NullValueHandling as ToJson does.
  • Tokens: StartArray, StartObject, PropertyName, Integer (a long), Float (a double, or a decimal as its exact text), String, Boolean, Null, Date (a UTC DateTime), Bytes, EndObject and EndArray; a json column's value comes as nested tokens. Depth and Path ([3].amount, [3].extra.tags[0]) are as in Newtonsoft.
  • Memory stays flat: the stream holds about 64 KiB of text at a time. 200,000 rows × 300 columns (1.3 GB of JSON from a 135 MB file): the stream 7.1 s at 63 MB; the token reader 4.8 s (120 million tokens) at 62 MB.

From the command line

npx jazmin convert transactions.csv transactions.jzm --sorted-by account,at
npx jazmin convert transactions.jzm march.csv --filter '{"at":{"gte":"2026-03-01","lt":"2026-04-01"}}'

See Command-Line Tool.

⚠️ **GitHub.com Fallback** ⚠️