json-alexander
Advanced tools
Comparing version 0.0.4 to 0.1.0
{ | ||
"name": "json-alexander", | ||
"version": "0.0.4", | ||
"description": "Serenity Now! Safely parse JSON", | ||
"version": "0.1.0", | ||
"description": "Serenity Now! Forgiving JSON parser", | ||
"main": "src/index.js", | ||
@@ -6,0 +6,0 @@ "scripts": { |
@@ -1,6 +0,6 @@ | ||
# Friendly JSON Parse | ||
# Forgiving JSON Parser | ||
<img align="right" src="https://user-images.githubusercontent.com/532272/64802133-d3d2b180-d53e-11e9-8182-101a1b927e29.jpg"> | ||
Serenity now! Safe `JSON.parse` | ||
Serenity now! A forgiving JSON parser 🙏 | ||
@@ -25,5 +25,9 @@ ```js | ||
// -> {"unbalanced": "object" } | ||
/* Javascript objects missing quotes */ | ||
parseJSON('{ hello: there }') | ||
// -> { "hello": "there" } | ||
``` | ||
Returns `undefined` if value passed in is not parsable | ||
Throws if value passed in is not parsable. | ||
@@ -38,2 +42,10 @@ ## Other options | ||
This package was built for nicer arg parser for CLIs. e.g. | ||
``` | ||
my-cli-command --data '{ foo: bar }' | ||
``` | ||
If you need a JSON parser for your server, consider the `safeParse` export instead. | ||
This package makes use of regular expressions when fixing malformed json. As a result, it may be vulnerable to a [REDOS attack](https://snyk.io/blog/redos-and-catastrophic-backtracking). | ||
@@ -40,0 +52,0 @@ |
187
src/index.js
const { isBalanced, trimQuotes, isNull } = require('./utils') | ||
module.exports.parseJSON = function parseJSON(x, defaultValue) { | ||
const value = (typeof x === 'string') ? coerceStr(x, defaultValue) : coerceToString(x) | ||
try { | ||
if (isNull(value) && defaultValue) return defaultValue | ||
return JSON.parse(value) | ||
} catch (e) { | ||
// console.log(`JSON.parse failed: ${e.message}`) | ||
// console.log(value) | ||
try { | ||
const stringify = JSON.stringify(trimQuotes(value)) | ||
return JSON.parse(stringify) | ||
} catch (e) { | ||
return defaultValue | ||
} | ||
} | ||
} | ||
const DEBUG = false | ||
const log = (DEBUG) ? log : () => {} | ||
@@ -34,3 +20,148 @@ module.exports.safeParse = function simpleParse(data, defaultValue) { | ||
function getType(value, defaultValue) { | ||
module.exports.parseJSON = function parseJSON(input, defaultValue) { | ||
if (isNull(input) || input === '' || input === undefined) { | ||
return defaultValue || input | ||
} | ||
const value = (typeof input === 'string') ? coerceStr(input, defaultValue) : coerceToString(input) | ||
if (value === 'true') { | ||
return true | ||
} | ||
if (value === 'false') { | ||
return false | ||
} | ||
const [err, first ] = parse(value) | ||
// log('trimmed', trimmed) | ||
if (first) { | ||
log('first', first) | ||
if (!needsMoreProcessing(first)) { | ||
return first | ||
} | ||
} | ||
const trimmed = trimQuotes(value) | ||
// log('trimmed', trimmed) | ||
const [errTwo, second ] = parse(trimmed) | ||
if (second) { | ||
log('second', second) | ||
if (!needsMoreProcessing(second)) { | ||
return second | ||
} | ||
} | ||
const fixString = convertStringObjectToJsonString(trimmed) | ||
// log('fixString', fixString) | ||
const [errThree, third ] = parse(fixString) | ||
if (third) { | ||
log('third', third) | ||
if (!needsMoreProcessing(third)) { | ||
return third | ||
} | ||
} | ||
const foo = coerceStr(fixString, defaultValue) | ||
log('foo', foo) | ||
const what = fixEscapedKeys(foo) | ||
log('what', what) | ||
const [errFour, four ] = parse(what) | ||
if (four) { | ||
log('four', four) | ||
if (!needsMoreProcessing(four)) { | ||
return four | ||
} | ||
} | ||
const final = trimQuotes(what.replace(/'/g, '"')) | ||
const [errFive, five ] = parse(final) | ||
if (five) { | ||
log('five', five) | ||
if (!needsMoreProcessing(five)) { | ||
return five | ||
} | ||
} | ||
// Wrap values missing quotes { cool: nice } | ||
const newer = final | ||
// Temporarily stash boolean values | ||
.replace(/:\s?(true+?)\s?/g, ': "TRUE_PLACEHOLDER"') | ||
.replace(/:\s?(false+?)\s?/g, ': "FALSE_PLACEHOLDER"') | ||
// .replace(/\[\s?(true+?)\s?\]/, 'TRUE_PLACEHOLDER') | ||
// .replace(/\[\s?(false+?)\s?\]/, 'FALSE_PLACEHOLDER') | ||
.replace(/\[\s?([A-Za-z]+?)\s?\]/, '[ "$1" ]') | ||
.replace(/:\s?([A-Za-z]+?)\s}/g, ': "$1" }') | ||
// log('newer', newer) | ||
// Wrap values missing quotes | ||
const newerStill = newer | ||
.replace(/:\s?([A-Za-z]+?)\s?,/g, ': "$1",') | ||
// Reset Temporarily stashed boolean values | ||
.replace(/:\s?("TRUE_PLACEHOLDER"+?)\s?/g, ': true') | ||
.replace(/:\s?("FALSE_PLACEHOLDER"+?)\s?/g, ': false') | ||
// trailing booleans | ||
// log('newerStill', newerStill) | ||
const [errSeven, six ] = parse(newerStill) | ||
if (six) { | ||
log('six', six) | ||
return six | ||
} | ||
// Attempt final rebalance | ||
const balance = coerceStr(newerStill, defaultValue) | ||
.replace(/\sfalse"}$/, ' false}') | ||
.replace(/\strue"}$/, ' true}') | ||
const [errSix, seven ] = parse(balance) | ||
if (seven) { | ||
log('seven', seven) | ||
return seven | ||
} | ||
throw new Error('Unable to parse JSON') | ||
} | ||
function fixEscapedKeys(value) { | ||
return value.replace(/\s\\"/g, '"').replace(/\\"\:/, '":') | ||
} | ||
function parse(value) { | ||
let result, error | ||
try { | ||
result = JSON.parse(value) | ||
} catch (err) { | ||
// log('err', err) | ||
error = err | ||
} | ||
return [ error, result ] | ||
} | ||
function convertStringObjectToJsonString(str) { | ||
return str.replace(/(\w+:)|(\w+ :)/g, (matchedStr) => { | ||
return '"' + matchedStr.substring(0, matchedStr.length - 1) + '":' | ||
}) | ||
} | ||
/* | ||
function convertValuesObjectToJsonString(str) { | ||
return str.replace(/:(\w+')|(:\w+ ')/g, (matchedStr) => { | ||
return ": '" + matchedStr.substring(0, matchedStr.length - 1) + "'" | ||
}) | ||
} | ||
*/ | ||
function needsMoreProcessing(value) { | ||
if (typeof value !== 'string') { | ||
return false | ||
} | ||
const stringType = getStringType(value) | ||
return stringType === 'array' || stringType === 'object' | ||
} | ||
function getStringType(value, defaultValue) { | ||
if (Array.isArray(value) || Array.isArray(defaultValue)) { | ||
@@ -57,3 +188,3 @@ return 'array' | ||
let rawVal = trimQuotes(str) | ||
const type = getType(str, defaultReturn) | ||
const type = getStringType(str, defaultReturn) | ||
if ((type === 'array' || type === 'object') && !isBalanced(str)) { | ||
@@ -95,5 +226,5 @@ // Find and try to fix mismatch brackets | ||
function invertQuotes(str, quoteType, objectType) { | ||
// console.log('Original', str) | ||
// log('Original', str) | ||
// const replaceOuterQuotes = new RegExp(`(${quoteType})(?=(?:[^${quoteType}]|${quoteType}[^]*${quoteType})*)`, 'g') | ||
// console.log('replaceOuterQuotes', replaceOuterQuotes) | ||
// log('replaceOuterQuotes', replaceOuterQuotes) | ||
const quotePairsRegex = new RegExp(`${quoteType}[^\\\\${quoteType}]*(\\\\${quoteType}[^\\\\${quoteType}]*)*${quoteType}`, 'g') | ||
@@ -109,3 +240,3 @@ | ||
.replace(/"/g, 'INNERDOUBLEQUOTE') | ||
// console.log('redactedString', redactedString) | ||
// log('redactedString', redactedString) | ||
const repInner = (objectType === 'array') ? '"' : `\\"` | ||
@@ -116,3 +247,3 @@ const fixed = redactedString | ||
.replace(/INNERDOUBLEQUOTE/g, repInner) | ||
// console.log('fixed', fixed) | ||
// log('fixed', fixed) | ||
return fixed | ||
@@ -126,7 +257,7 @@ } | ||
const replaceInnerConflict = new RegExp(`${quoteType}`, 'g') | ||
// console.log('replaceInnerConflict', replaceInnerConflict) | ||
// log('replaceInnerConflict', replaceInnerConflict) | ||
const replaceInverseStart = new RegExp(`^${inverse}`) | ||
// console.log('replaceInverseStart', replaceInverseStart) | ||
// log('replaceInverseStart', replaceInverseStart) | ||
const replaceInverseEnd = new RegExp(`${inverse}$`) | ||
// console.log('replaceInverseEnd', replaceInverseEnd) | ||
// log('replaceInverseEnd', replaceInverseEnd) | ||
const fix = curr | ||
@@ -151,11 +282,11 @@ // replace inner " | ||
if (Array.isArray(val)) { | ||
// console.log(`Converting Array into string`) | ||
// log(`Converting Array into string`) | ||
return JSON.stringify(val) | ||
} | ||
if (type === 'object') { | ||
// console.log(`Converting Object into string`) | ||
// log(`Converting Object into string`) | ||
return JSON.stringify(val) | ||
} | ||
if (type === 'boolean') { | ||
// console.log(`Converting Boolean into string`) | ||
// log(`Converting Boolean into string`) | ||
return JSON.stringify(val) | ||
@@ -162,0 +293,0 @@ } |
13120
335
76