Created
November 26, 2025 02:32
-
-
Save Zezombye/eea0f075e517fe7aa363a273ddc1bf01 to your computer and use it in GitHub Desktop.
Javascript JSON validator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //Given a string, returns true if the string is the beginning of a valid JSON string. | |
| //This function assumes the string can get cut off, such that "tru" is valid as it is the beginning of "true". | |
| //Same for unclosed strings, arrays, and objects. | |
| function jsonValidate(str) { | |
| let i = 0; | |
| const len = str.length; | |
| const stack = []; // Tracks context: '{' or '[' | |
| // Tracks the last successfully parsed token to enforce grammar | |
| // States: 'START', 'OBJ_OPEN', 'ARR_OPEN', 'KEY', 'COLON', 'VALUE', 'COMMA' | |
| let lastToken = 'START'; | |
| let hasSeenNonWhitespace = false; //don't allow whitespace-only string | |
| while (i < len) { | |
| // 1. Skip Whitespace | |
| if (/\s/.test(str[i])) { | |
| i++; | |
| continue; | |
| } | |
| hasSeenNonWhitespace = true; | |
| const char = str[i]; | |
| const context = stack.length > 0 ? stack[stack.length - 1] : 'ROOT'; | |
| // 2. Handle Structural Characters | |
| // Start Object | |
| if (char === '{') { | |
| // Valid contexts: Start, after Colon (in obj), or after Bracket/Comma (in array) | |
| if (context === 'ROOT' && lastToken !== 'START') return false; | |
| if (context === '{' && lastToken !== 'COLON') return false; | |
| if (context === '[' && lastToken !== 'ARR_OPEN' && lastToken !== 'COMMA') return false; | |
| stack.push('{'); | |
| lastToken = 'OBJ_OPEN'; | |
| i++; | |
| continue; | |
| } | |
| // Start Array | |
| if (char === '[') { | |
| if (context === 'ROOT' && lastToken !== 'START') return false; | |
| if (context === '{' && lastToken !== 'COLON') return false; | |
| if (context === '[' && lastToken !== 'ARR_OPEN' && lastToken !== 'COMMA') return false; | |
| stack.push('['); | |
| lastToken = 'ARR_OPEN'; | |
| i++; | |
| continue; | |
| } | |
| // End Object | |
| if (char === '}') { | |
| if (context !== '{') return false; | |
| // Valid if object is empty OR if we just finished a value | |
| if (lastToken !== 'OBJ_OPEN' && lastToken !== 'VALUE') return false; | |
| stack.pop(); | |
| lastToken = 'VALUE'; // The completed object acts as a Value in the parent | |
| i++; | |
| continue; | |
| } | |
| // End Array | |
| if (char === ']') { | |
| if (context !== '[') return false; | |
| // Valid if array is empty OR if we just finished a value | |
| if (lastToken !== 'ARR_OPEN' && lastToken !== 'VALUE') return false; | |
| stack.pop(); | |
| lastToken = 'VALUE'; | |
| i++; | |
| continue; | |
| } | |
| // Colon | |
| if (char === ':') { | |
| if (context !== '{') return false; | |
| if (lastToken !== 'KEY') return false; // Must follow a Key | |
| lastToken = 'COLON'; | |
| i++; | |
| continue; | |
| } | |
| // Comma | |
| if (char === ',') { | |
| if (context === 'ROOT') return false; | |
| if (lastToken !== 'VALUE') return false; // Must follow a Value | |
| lastToken = 'COMMA'; | |
| i++; | |
| continue; | |
| } | |
| // 3. Handle Strings (Keys or Values) | |
| if (char === '"') { | |
| // Determine if this string is a Key or a Value based on context | |
| let isKey = false; | |
| if (context === '{') { | |
| if (lastToken === 'OBJ_OPEN' || lastToken === 'COMMA') isKey = true; | |
| else if (lastToken === 'COLON') isKey = false; | |
| else return false; // e.g. "key" "key" missing colon | |
| } else if (context === '[') { | |
| if (lastToken === 'ARR_OPEN' || lastToken === 'COMMA') isKey = false; | |
| else return false; | |
| } else { // ROOT | |
| if (lastToken === 'START') isKey = false; | |
| else return false; | |
| } | |
| i++; // Skip opening quote | |
| // Consume String | |
| while (i < len) { | |
| if (str[i] === '\\') { | |
| i++; | |
| if (i < len && '"/\\bfnrt'.includes(str[i])) { | |
| // Valid escape, continue | |
| i++; | |
| } else if (i + 4 < len && str[i] === 'u' && /^[0-9a-fA-F]{4}$/.test(str.substring(i+1, i+5))) { | |
| // Valid unicode escape | |
| i += 4; // Skip the 4 hex digits | |
| } else { | |
| return false; // Invalid escape sequence | |
| } | |
| continue; | |
| } | |
| if (i < len && str[i] === '"') { | |
| break; // Found closing quote | |
| } | |
| i++; | |
| } | |
| if (i >= len) return true; // Valid partial: string cut off | |
| i++; // Skip closing quote | |
| lastToken = isKey ? 'KEY' : 'VALUE'; | |
| continue; | |
| } | |
| // 4. Handle Numbers | |
| if (char === '-' || char === "+" ||/[0-9]/.test(char)) { | |
| if (context === 'ROOT' && lastToken !== 'START') return false; | |
| if (context === '{' && lastToken !== 'COLON') return false; | |
| if (context === '[' && lastToken !== 'ARR_OPEN' && lastToken !== 'COMMA') return false; | |
| const start = i; | |
| // Quick check for invalid leading zeros (e.g., 01, -01) which are not allowed in JSON | |
| if (char === '0' && i + 1 < len && /[0-9]/.test(str[i+1])) return false; | |
| if ((char === '-' || char === "+") && i + 1 < len && str[i+1] === '0' && i + 2 < len && /[0-9]/.test(str[i+2])) return false; | |
| let hasSeenDot = false; | |
| let lastCharWasDot = false; | |
| let hasSeenDigit = false; | |
| let canHavePlusOrMinus = true; | |
| let hasSeenExp = false; | |
| // Consume valid number characters greedily | |
| while (i < len && /[0-9eE+\-\.]/.test(str[i])) { | |
| if (str[i] === ".") { | |
| if (hasSeenDot || hasSeenExp || !hasSeenDigit) return false; // Only one dot allowed, and not after exponent | |
| hasSeenDot = true; | |
| lastCharWasDot = true; | |
| canHavePlusOrMinus = false; | |
| } else if (/[0-9]/.test(str[i])) { | |
| hasSeenDigit = true; | |
| lastCharWasDot = false; | |
| canHavePlusOrMinus = false; | |
| } else if (/[eE]/.test(str[i])) { | |
| if (hasSeenExp || !hasSeenDigit || lastCharWasDot) return false; // Only one exponent allowed, must follow digit | |
| hasSeenExp = true; | |
| hasSeenDigit = false; // Need digit after exponent | |
| lastCharWasDot = false; | |
| canHavePlusOrMinus = true; // Can have + or - after exponent | |
| } else if (str[i] === '+' || str[i] === '-') { | |
| // Sign only allowed immediately after exponent or start of number | |
| if (!canHavePlusOrMinus) return false; | |
| canHavePlusOrMinus = false; | |
| lastCharWasDot = false; | |
| } else { | |
| lastCharWasDot = false; | |
| } | |
| i++; | |
| } | |
| // If we hit EOF while parsing number, it is valid partial (e.g., "1.", "1e", "-") | |
| if (i >= len) return true; | |
| // If we stopped due to a separator, strict check the formed number | |
| const numStr = str.substring(start, i); | |
| if (!/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/.test(numStr)) return false; | |
| lastToken = 'VALUE'; | |
| continue; | |
| } | |
| // 5. Handle Keywords (true, false, null) | |
| if (['t', 'f', 'n'].includes(char)) { | |
| if (context === 'ROOT' && lastToken !== 'START') return false; | |
| if (context === '{' && lastToken !== 'COLON') return false; | |
| if (context === '[' && lastToken !== 'ARR_OPEN' && lastToken !== 'COMMA') return false; | |
| const target = char === 't' ? 'true' : (char === 'f' ? 'false' : 'null'); | |
| let j = 0; | |
| while (j < target.length && i < len) { | |
| if (str[i] !== target[j]) return false; // Char mismatch (e.g., "falze") | |
| i++; | |
| j++; | |
| } | |
| if (j < target.length) return true; // Valid partial (e.g., "tru", "fa") | |
| lastToken = 'VALUE'; | |
| continue; | |
| } | |
| // Invalid Character | |
| return false; | |
| } | |
| if (!hasSeenNonWhitespace) return false; // Don't allow whitespace-only string | |
| // If loop finishes without error, the partial string is valid | |
| return true; | |
| } | |
| let testSuite = { | |
| [`{"a": "b", "c": ["d", true]}`]: true, | |
| [`{"a": "b", "c": ["d", true]`]: true, | |
| [`{"a": "b", "c": ["d", true]]`]: false, | |
| [`{"a": "b", "c": ["d", true`]: true, | |
| [`{"a": "b", "c": ["d", tru`]: true, | |
| [`{"a": "b", "c": ["d", tr`]: true, | |
| [`{"a": "b", "c": ["d", t`]: true, | |
| [`{"a": "b", "c": ["d", tre`]: false, | |
| [`{"a": "b", "c": ["d",`]: true, | |
| [`{"a": "b", "c": ["d",,`]: false, | |
| [`{"a": "b", "c": ["d"`]: true, | |
| [`{"a": "b", "c": ["d":`]: false, | |
| [`{"a": "b", "c": ["d`]: true, | |
| [`{"a": "b", "c": ["`]: true, | |
| [`{"a": "b", "c": [`]: true, | |
| [`{"a": "b", "c":: [`]: false, | |
| [`{"a": "b", "c": `]: true, | |
| [`{"a": "b", "c"`]: true, | |
| [`{"a": "b", "c`]: true, | |
| [`{"a": "b", "`]: true, | |
| [`{"a": "b", `]: true, | |
| [`{"a": "b",, `]: false, | |
| [`{"a": "b"`]: true, | |
| [`{"a": "b`]: true, | |
| [`{"a": "b\\"c`]: true, | |
| [`{"a": "b\\"c""`]: false, | |
| [`{"a": "b\\p`]: false, | |
| [`{"a": "b\\b`]: true, | |
| [`{"a": "b\\f`]: true, | |
| [`{"a": "b\\n`]: true, | |
| [`{"a": "b\\r`]: true, | |
| [`{"a": "b\\t`]: true, | |
| [`{"a": "b\\\\`]: true, | |
| [`{"a": "b\\u12aB`]: true, | |
| [`{"a": "b\\u12aBp`]: true, | |
| [`{"a": "b\\u12ap`]: false, | |
| [`{"a": "b\\N`]: false, | |
| [`{"a": 1`]: true, | |
| [`{"a": 1.`]: true, | |
| [`{"a": 1.0e`]: true, | |
| [`{"a": 1.0e+`]: true, | |
| [`{"a": 1.0e+3`]: true, | |
| [`{"a": 1.0e+3.`]: false, | |
| [`{"a": 1.0e+3+`]: false, | |
| [`{"a": 1.0e+3-`]: false, | |
| [`{"a": 1.0e+3e`]: false, | |
| [`{"a": 1.0e-3`]: true, | |
| [`{"a": 1e-3`]: true, | |
| [`{"a": 1..`]: false, | |
| [`{"a": 1.2.`]: false, | |
| [`{"a": -`]: true, | |
| [`{"a": +`]: true, | |
| [`{"a": -.`]: false, | |
| [`{"a": +.`]: false, | |
| [`{"a": +-`]: false, | |
| [`{"a": -+`]: false, | |
| [`{"a": ++`]: false, | |
| [`{"a": --`]: false, | |
| [`{"a": 1-`]: false, | |
| [`{"a": 1+`]: false, | |
| [`{"a": 1e3-`]: false, | |
| [`{"a": 1e3+`]: false, | |
| [`{"a": .`]: false, | |
| [`{"a": -e4`]: false, | |
| [`{"a": +e4`]: false, | |
| [`"abc`]: true, | |
| [`"abc""`]: false, | |
| [`"abc"0`]: false, | |
| [` "abc`]: true, | |
| [` `]: false, | |
| [``]: false, | |
| [`0.`]: true, | |
| [`t`]: true, | |
| [`f`]: true, | |
| [`n`]: true, | |
| [`nul`]: true, | |
| [`nulf`]: false, | |
| [`nulll`]: false, | |
| [`[[[`]: true, | |
| [`[[[}`]: false, | |
| } | |
| let hasError = false; | |
| for (let [input, expected] of Object.entries(testSuite)) { | |
| let result = jsonValidate(input); | |
| if (result !== expected) { | |
| console.error(`Test failed for input: ${input}. Expected: ${expected}, but got: ${result}`); | |
| hasError = true; | |
| } | |
| } | |
| if (!hasError) { | |
| console.log("All tests passed!"); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment