Skip to content

Commit b05e70d

Browse files
hakimilgsharevb
andauthored
feat(new tool): JSON to .env converter (#349)
Converts a flat JSON object into a .env file. Keys are upper-cased, values are safely quoted when they contain whitespace, quotes, or env special characters (#, $, `, \). Nested objects and arrays are JSON-stringified and always quoted; null/undefined become "". Follows the existing FormatTransformer pattern used by the other json-to-* converters, including i18n via useI18n and the built-in .env download button. Co-authored-by: ShareVB <sharevb@gmail.com>
1 parent f7f82a5 commit b05e70d

6 files changed

Lines changed: 227 additions & 0 deletions

File tree

locales/en.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,14 @@ tools:
551551
input-label-your-json: Your JSON
552552
input-placeholder-paste-your-json-here: Paste your JSON here...
553553
output-label-yaml-from-your-json: YAML from your JSON
554+
json-to-env:
555+
title: JSON to .env
556+
description: Convert a flat JSON object into a .env file. Keys are upper-cased and values are safely quoted when they contain whitespace, quotes, or env-special characters.
557+
texts:
558+
message-provided-json-is-not-a-valid-flat-object: Provided JSON is not a valid flat object.
559+
input-label-your-json: Your JSON
560+
input-placeholder-paste-your-json-here: Paste your JSON here...
561+
output-label-env-from-your-json: .env from your JSON
554562
json-to-javascript:
555563
title: JSON to JS converter
556564
description: Simply convert JSON to Javascript object with this live online converter.

src/tools/json-to-env/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Braces } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
import { translate as t } from '@/plugins/i18n.plugin';
4+
5+
export const tool = defineTool({
6+
name: t('tools.json-to-env.title'),
7+
path: '/json-to-env',
8+
description: t('tools.json-to-env.description'),
9+
keywords: ['json', 'to', 'env', 'dotenv', 'convert', 'environment', 'variables'],
10+
component: () => import('./json-to-env.vue'),
11+
icon: Braces,
12+
createdAt: new Date('2026-04-17'),
13+
npmPackages: ['json5'],
14+
category: 'JSON',
15+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.describe('Tool - JSON to .env', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/json-to-env');
6+
});
7+
8+
test('Has correct title', async ({ page }) => {
9+
await expect(page).toHaveTitle('JSON to .env - IT Tools');
10+
});
11+
12+
test('Provided JSON is converted to .env', async ({ page }) => {
13+
await page.getByTestId('input').fill(`{
14+
"ACCESS_KEY": "mySecretAccessKey",
15+
"AMQP_DNS": "amqp://a:b@rabbit/po",
16+
"APP_ENV": "prod"
17+
}`);
18+
19+
const generated = await page.getByTestId('area-content').innerText();
20+
21+
expect(generated.trim()).toEqual(
22+
`ACCESS_KEY=mySecretAccessKey
23+
AMQP_DNS=amqp://a:b@rabbit/po
24+
APP_ENV=prod`.trim(),
25+
);
26+
});
27+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { convertJsonToEnv } from './json-to-env.service';
3+
4+
describe('json-to-env service', () => {
5+
describe('convertJsonToEnv', () => {
6+
it('converts a flat JSON object to .env lines', () => {
7+
const json = JSON.stringify({
8+
ACCESS_KEY: 'mySecretAccessKey',
9+
APP_ENV: 'prod',
10+
});
11+
12+
expect(convertJsonToEnv({ json })).toMatchInlineSnapshot(`
13+
"ACCESS_KEY=mySecretAccessKey
14+
APP_ENV=prod"
15+
`);
16+
});
17+
18+
it('uppercases keys by default', () => {
19+
expect(convertJsonToEnv({ json: '{"app_env": "prod"}' })).toBe('APP_ENV=prod');
20+
});
21+
22+
it('keeps original case when uppercaseKeys is false', () => {
23+
expect(convertJsonToEnv({ json: '{"appEnv": "prod"}', uppercaseKeys: false })).toBe('appEnv=prod');
24+
});
25+
26+
it('quotes values that contain spaces', () => {
27+
expect(convertJsonToEnv({ json: '{"greeting": "hello world"}' })).toBe('GREETING="hello world"');
28+
});
29+
30+
it('quotes values that contain double quotes and escapes them', () => {
31+
expect(convertJsonToEnv({ json: '{"q": "he said \\"hi\\""}' })).toBe('Q="he said \\"hi\\""');
32+
});
33+
34+
it('quotes values that contain # so they are not read as comments', () => {
35+
expect(convertJsonToEnv({ json: '{"color": "#ff0000"}' })).toBe('COLOR="#ff0000"');
36+
});
37+
38+
it('quotes empty string values', () => {
39+
expect(convertJsonToEnv({ json: '{"empty": ""}' })).toBe('EMPTY=""');
40+
});
41+
42+
it('converts null and undefined to empty values', () => {
43+
expect(convertJsonToEnv({ json: '{"a": null}' })).toBe('A=""');
44+
});
45+
46+
it('stringifies numbers and booleans', () => {
47+
expect(convertJsonToEnv({ json: '{"port": 8080, "debug": true}' })).toMatchInlineSnapshot(`
48+
"PORT=8080
49+
DEBUG=true"
50+
`);
51+
});
52+
53+
it('serializes nested objects and arrays as JSON strings', () => {
54+
const result = convertJsonToEnv({ json: '{"list": [1, 2, 3], "obj": {"a": 1}}' });
55+
expect(result).toMatchInlineSnapshot(`
56+
"LIST=\\"[1,2,3]\\"
57+
OBJ=\\"{\\\\\\"a\\\\\\":1}\\""
58+
`);
59+
});
60+
61+
it('escapes newlines in values', () => {
62+
expect(convertJsonToEnv({ json: '{"multi": "line1\\nline2"}' })).toBe('MULTI="line1\\nline2"');
63+
});
64+
65+
it('returns empty string on empty input', () => {
66+
expect(convertJsonToEnv({ json: '' })).toBe('');
67+
expect(convertJsonToEnv({ json: ' ' })).toBe('');
68+
});
69+
70+
it('throws when JSON is an array', () => {
71+
expect(() => convertJsonToEnv({ json: '[1, 2, 3]' })).toThrow();
72+
});
73+
74+
it('throws when JSON is a primitive', () => {
75+
expect(() => convertJsonToEnv({ json: '"just a string"' })).toThrow();
76+
});
77+
78+
it('throws on invalid JSON', () => {
79+
expect(() => convertJsonToEnv({ json: '{not json}' })).toThrow();
80+
});
81+
});
82+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import JSON5 from 'json5';
2+
3+
export { convertJsonToEnv };
4+
5+
function serializeValue(value: unknown): { value: string; forceQuote: boolean } {
6+
if (value === null || value === undefined) {
7+
return { value: '', forceQuote: true };
8+
}
9+
10+
if (typeof value === 'object') {
11+
return { value: JSON.stringify(value), forceQuote: true };
12+
}
13+
14+
return { value: String(value), forceQuote: false };
15+
}
16+
17+
function formatEnvValue({ value, forceQuote }: { value: string; forceQuote: boolean }): string {
18+
const needsQuoting = forceQuote || value === '' || /[\s"'#`$\\]/.test(value);
19+
20+
if (!needsQuoting) {
21+
return value;
22+
}
23+
24+
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
25+
return `"${escaped}"`;
26+
}
27+
28+
function convertJsonToEnv({ json, uppercaseKeys = true }: { json: string; uppercaseKeys?: boolean }): string {
29+
const trimmed = json.trim();
30+
31+
if (trimmed === '') {
32+
return '';
33+
}
34+
35+
const parsed = JSON5.parse(trimmed);
36+
37+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
38+
throw new TypeError('JSON must be an object, not an array or primitive.');
39+
}
40+
41+
return Object.entries(parsed)
42+
.map(([key, value]) => {
43+
const envKey = uppercaseKeys ? key.toUpperCase() : key;
44+
const envValue = formatEnvValue(serializeValue(value));
45+
46+
return `${envKey}=${envValue}`;
47+
})
48+
.join('\n');
49+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script setup lang="ts">
2+
import { useI18n } from 'vue-i18n';
3+
import JSON5 from 'json5';
4+
import { convertJsonToEnv } from './json-to-env.service';
5+
import type { UseValidationRule } from '@/composable/validation';
6+
import { withDefaultOnError } from '@/utils/defaults';
7+
8+
const { t } = useI18n();
9+
10+
const defaultValue = '{\n "ACCESS_KEY": "mySecretAccessKey",\n "AMQP_DNS": "amqp://a:b@rabbit/po",\n "APP_ENV": "prod"\n}';
11+
12+
function transformer(value: string) {
13+
return withDefaultOnError(() => {
14+
if (value === '') {
15+
return '';
16+
}
17+
return convertJsonToEnv({ json: value });
18+
}, '');
19+
}
20+
21+
const rules: UseValidationRule<string>[] = [
22+
{
23+
validator: (v: string) => {
24+
if (v === '') {
25+
return true;
26+
}
27+
const parsed = JSON5.parse(v);
28+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed);
29+
},
30+
message: t('tools.json-to-env.texts.message-provided-json-is-not-a-valid-flat-object'),
31+
},
32+
];
33+
</script>
34+
35+
<template>
36+
<format-transformer
37+
:input-label="t('tools.json-to-env.texts.input-label-your-json')"
38+
:input-default="defaultValue"
39+
:input-placeholder="t('tools.json-to-env.texts.input-placeholder-paste-your-json-here')"
40+
:output-label="t('tools.json-to-env.texts.output-label-env-from-your-json')"
41+
output-language="bash"
42+
:input-validation-rules="rules"
43+
:transformer="transformer"
44+
download-file-name=".env"
45+
/>
46+
</template>

0 commit comments

Comments
 (0)