├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.test.ts └── index.ts ├── test └── test.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | coverage 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2014 NOOK Media, LLC and (c) 2018 Donavon West 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json\_ 2 | 3 | [![Build Status](https://travis-ci.org/donavon/json_.svg?branch=master)](https://travis-ci.org/donavon/json_) [![npm version](https://img.shields.io/npm/v/json_.svg)](https://www.npmjs.com/package/json_) 4 | 5 | Converts camelCase JavaScript objects to JSON snake_case and vise versa. This is a direct replacement for the built-in JSON object. In fact, this simply wraps the built-in JSON object. Very handy when your back_end APIs are not build using Node.js. 6 | It also supports converting a `fetch` response stream into a camelCased object. 7 | 8 | > NOTE: New version 3.0 completely re-written in TypeScript, so it's fully typed. 9 | 10 | ## First, get the package 11 | 12 | Install json\_ and include it in your build. 13 | 14 | ```bash 15 | npm install json_ --save 16 | ``` 17 | 18 | Then import it like this. 19 | 20 | ```js 21 | import JSON_ from 'json_'; 22 | ``` 23 | 24 | ## Example 25 | 26 | ```js 27 | const example = { 28 | firstName: 'John', 29 | lastName: 'Doe', 30 | isbn10: '1234567890', 31 | }; 32 | 33 | console.log(JSON_.stringify(example)); 34 | // {"first_name":"John","last_name":"Doe", "isbn_10": "1234567890"} 35 | ``` 36 | 37 | And vise versa. 38 | 39 | ```js 40 | import JSON_ from 'json_'; 41 | const str = '{"ultimate_answer": 42}'; 42 | 43 | console.log(JSON_.parse(str)); 44 | // {ultimateAnswer: 42} 45 | ``` 46 | 47 | ## Using with `fetch` 48 | 49 | You can use `json_` directly with the JavaScript `fetch` API to convert 50 | the `Response` into an `Object` with snakeCase. 51 | 52 | Let's say you have a function that returns snake_case weather data, something like this. 53 | 54 | ```js 55 | const fetchWeather = async zip => { 56 | const response = fetch(`${weatherUrl}?zip=${zip}`); 57 | const json = await response.json(); 58 | return json; 59 | }; 60 | 61 | const data = await fetchWeather('10285'); 62 | console.log(data); 63 | // {current_temp: 85, reporting_station: 'New York, NY'} 64 | ``` 65 | 66 | You can easily convert the resolved object to camelCase by replacing the call to `Response.json()` 67 | to a call to `JSON_.parse(Response)`, like this. 68 | 69 | ```diff 70 | - const json = await response.json(); 71 | + const json = await JSON_.parse(response); 72 | ``` 73 | 74 | The resulting code looks like this 75 | 76 | ```js 77 | import JSON_ from 'json_'; 78 | 79 | const fetchWeather = async zip => { 80 | const response = fetch(`${weatherUrl}?zip=${zip}`); 81 | const json = await JSON_.parse(response); 82 | return json; 83 | }; 84 | 85 | const data = await fetchWeather('10285'); 86 | console.log(data); 87 | // {currentTemp: 85, reportingStation: 'New York, NY'} 88 | ``` 89 | 90 | ## Tests! 91 | 92 | 100% unit test coverage. To run the unit tests and get a coverage report: 93 | 94 | ``` 95 | npm test coverage 96 | ``` 97 | 98 | ## License 99 | 100 | Copyright Donavon West. Released under MIT license 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.0.1", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=14" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/donavon/json_.git" 16 | }, 17 | "bugs": { 18 | "url": "http://github.com/donavon/json_/issues" 19 | }, 20 | "scripts": { 21 | "start": "tsdx watch", 22 | "build": "tsdx build", 23 | "test": "tsdx test --passWithNoTests", 24 | "coverage": "tsdx test --coverage", 25 | "lint": "tsdx lint", 26 | "prepare": "tsdx build", 27 | "size": "size-limit", 28 | "analyze": "size-limit --why" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "pre-commit": "tsdx lint" 33 | } 34 | }, 35 | "prettier": { 36 | "printWidth": 80, 37 | "semi": true, 38 | "singleQuote": true, 39 | "trailingComma": "es5" 40 | }, 41 | "name": "json_", 42 | "description": "Converts camelCase JavaScript objects to JSON snake_case and vise versa.", 43 | "author": "Donavon West (https://donavon.com/)", 44 | "keywords": [ 45 | "JSON", 46 | "snakecase", 47 | "camelcase", 48 | "fetch", 49 | "parse", 50 | "stringify" 51 | ], 52 | "module": "dist/json_.esm.js", 53 | "size-limit": [ 54 | { 55 | "path": "dist/json_.cjs.production.min.js", 56 | "limit": "10 KB" 57 | }, 58 | { 59 | "path": "dist/json_.esm.js", 60 | "limit": "10 KB" 61 | } 62 | ], 63 | "devDependencies": { 64 | "@remix-run/node": "^1.3.3", 65 | "@size-limit/preset-small-lib": "^7.0.8", 66 | "husky": "^7.0.4", 67 | "size-limit": "^7.0.8", 68 | "tsdx": "^0.14.1", 69 | "tslib": "^2.3.1", 70 | "typescript": "^4.6.2" 71 | }, 72 | "jest": { 73 | "coverageThreshold": { 74 | "global": { 75 | "statements": 100, 76 | "branches": 100, 77 | "functions": 100, 78 | "lines": 100 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import JSON_ from '.'; 2 | import { installGlobals } from '@remix-run/node'; 3 | 4 | // This installs globals such as "fetch", "Response", "Request" and "Headers". 5 | installGlobals(); 6 | 7 | const testCases = [ 8 | ['first_name', 'firstName'], 9 | ['address_1', 'address1'], 10 | ['scores_a_1_b_2', 'scoresA1B2'], 11 | ]; 12 | 13 | describe('JSON_', function() { 14 | describe('parse', function() { 15 | describe('when passed a string', function() { 16 | testCases.forEach(([snakeCasedKey, camelCasedKey]) => { 17 | it(`should return ${camelCasedKey} given ${snakeCasedKey}`, function() { 18 | const results = JSON_.parse(`{"${snakeCasedKey}": ""}`); 19 | const expected = JSON.parse(`{"${camelCasedKey}": ""}`); 20 | expect(results).toEqual(expected); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('when passed a Response', function() { 26 | it('should return a promise', function() { 27 | const response = new Response('{ "foo_bar": 1 }'); 28 | const promise = JSON_.parse(response); 29 | expect(promise).toBeInstanceOf(Promise); 30 | }); 31 | it('that resolves with a camelCased JS object', async () => { 32 | const response = new Response('{ "foo_bar": 1 }'); 33 | const obj = await JSON_.parse(response); 34 | expect(obj).toEqual({ fooBar: 1 }); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('stringify', function() { 40 | testCases.forEach(([snakeCasedKey, camelCasedKey]) => { 41 | it(`should return ${snakeCasedKey} given ${camelCasedKey}`, function() { 42 | const results = JSON_.stringify({ [camelCasedKey]: '' }); 43 | const expected = JSON.stringify({ [snakeCasedKey]: '' }); 44 | expect(results).toBe(expected); 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | type Parse = typeof JSON.parse; 2 | type Reviver = Parameters[1]; 3 | type Stringify = typeof JSON.stringify; 4 | type Replacer = Parameters[1]; 5 | 6 | const snakeToCamelCase = (key: string) => 7 | // first_name -> firstName, isbn_10 -> isbn10, scores_a_1 -> scoresA1 8 | key.replace(/(_+[a-z0-9])/g, (snip: string) => 9 | snip.toUpperCase().replace('_', '') 10 | ); 11 | 12 | const camelToSnakeCase = (key: string) => 13 | // firstName -> first_name, isbn10 -> isbn_10, scoresA1 -> scores_a_1 14 | key 15 | .replace(/([A-Za-z])([0-9])/g, (_unused, char, digit) => char + '_' + digit) 16 | .replace(/([A-Z])/g, snip => '_' + snip.toLowerCase()); 17 | 18 | function parse(text: string | Response, reviver?: Reviver): any; 19 | function parse(response: Response, reviver?: Reviver): Promise; 20 | function parse(responseOrText: string | Response, reviver?: Reviver): any { 21 | if (typeof responseOrText === 'string') { 22 | const text = responseOrText.replace(/"([^"]*)"\s*:/g, snakeToCamelCase); 23 | return JSON.parse(text, reviver); 24 | } 25 | return responseOrText 26 | .text() 27 | .then((text: string) => JSON_.parse(text, reviver)); // Assume text is a Response object from fetch. 28 | } 29 | 30 | function stringify(value: any, replacer?: Replacer, space?: number): string { 31 | const str = JSON.stringify(value, replacer, space); 32 | return str.replace(/"([^"]*)"\s*:/g, camelToSnakeCase); 33 | } 34 | 35 | const JSON_ = { 36 | parse, 37 | stringify, 38 | }; 39 | 40 | export default JSON_; 41 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var JSON_ = require("../."); 3 | 4 | var responseStub = { 5 | text: function() { 6 | return Promise.resolve('{ "foo_bar": 1 }'); 7 | } 8 | }; 9 | 10 | describe('JSON_', function(){ 11 | describe('parse (passing a string)', function (){ 12 | it('should return camelCase property names given snake_case JSON', function (){ 13 | var data; 14 | data = JSON_.parse('{"first_name": ""}'); 15 | assert.equal(Object.keys(data)[0], "firstName"); 16 | 17 | data = JSON_.parse('{"isbn_10": ""}'); 18 | assert.equal(Object.keys(data)[0], "isbn10"); 19 | 20 | data = JSON_.parse('{"scores_a_1": ""}'); 21 | assert.equal(Object.keys(data)[0], "scoresA1"); 22 | }); 23 | }); 24 | describe('parse (passing a Response)', function (){ 25 | it('should return a promise', function (){ 26 | assert.equal(JSON_.parse(responseStub) instanceof Promise, true); 27 | }); 28 | it('that resolves with a camelCased JS object', function (done){ 29 | JSON_.parse(responseStub).then(function (obj) { 30 | assert.equal(JSON.stringify(obj), JSON.stringify({ fooBar: 1 })); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | describe('stringify', function (){ 36 | it('should return snake_case JSON given an object with camelCase property names', function (){ 37 | var data; 38 | data = JSON_.stringify({firstName: ""}); 39 | assert.notEqual(data.indexOf("first_name"), -1); 40 | 41 | data = JSON_.stringify({isbn10: ""}); 42 | assert.notEqual(data.indexOf("isbn_10"), -1); 43 | 44 | data = JSON_.stringify({scoresA1: ""}); 45 | assert.notEqual(data.indexOf("scores_a_1"), -1); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | --------------------------------------------------------------------------------