🎃

【エラー解決】SyntaxError: Unexpected non-whitespace character after JSON

に公開

前提

以下のアルゴリズム問題を解いていた時に発生したエラーです。

問題概要

整数 n が与えられたとき、以下の条件に基づいて出力を生成するラムダ関数 fizzbuzz を作成する

  • n が 3 の倍数であれば "Fizz" を生成
  • n が 5 の倍数であれば "Buzz" を生成
  • n が 15 の倍数であれば "FizzBuzz" を生成
  • 上記のどの条件にも当てはまらない場合は、数値 n 自体を生成

作成した関数を用いて、提供されたテストケースで関数の出力を表示すること
なお、出力は 1 から n までの各数値に対して上記のルールに基づき生成し、各値はハイフン - で区切って連結

テストケース

ケース 入力 出力
1 9 1-2-Fizz-4-Buzz-Fizz-7-8-Fizz
2 20 1-2-Fizz-4-Buzz-Fizz-7-8-Fizz-Buzz-11-Fizz-13-14-FizzBuzz-16-17-Fizz-19-Buzz

使用言語: JavaScript

エラー内容

テスト出力結果
undefined:1
1-2-Fizz-4-Buzz-Fizz-7-8-Fizz
 ^

SyntaxError: Unexpected non-whitespace character after JSON at position 1
    at JSON.parse (<anonymous>)
    at Object.<anonymous> (/Users/mavo/project/algorithm-solutions/HigherOrderFunc/problems/01_fizzbuzz/js/tests/fizzBuzzTest.js:16:30)
    at Module._compile (node:internal/modules/cjs/loader:1369:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1427:10)
    at Module.load (node:internal/modules/cjs/loader:1206:32)
    at Module._load (node:internal/modules/cjs/loader:1022:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)
    at node:internal/main/run_main_module:28:49

Node.js v20.12.2

実装コード

fizzbuzz(ラムダ関数)
let fizzbuzz = n => {
    let resStr = "";

    for(let i = 1; i <= n; i++){
        if(i % 15 === 0) resStr += "-FizzBuzz";
        else if(i % 5 === 0) resStr += "-Buzz";
        else if(i % 3 === 0) resStr += "-Fizz";
        else resStr += `-${i}`;
    }

    return resStr.slice(1);
}

module.exports = fizzbuzz;
テストコード
const fizzbuzz = require('../src/fizzbuzz.js')

// テストケース
const tests = {
   "case1" : {
       "input" : 9,
       "output" : "1-2-Fizz-4-Buzz-Fizz-7-8-Fizz"
   },
   "case2" : {
       "input" : 20,
       "output" : "1-2-Fizz-4-Buzz-Fizz-7-8-Fizz-Buzz-11-Fizz-13-14-FizzBuzz-16-17-Fizz-19-Buzz"
   }
};

// 検証用ループ
for(let [key, value] of Object.entries(tests)){
    const res = fizzbuzz(value['input']);
    const finalResult = JSON.parse(res) === JSON.parse(value['output']) ? "True" : "False";
    console.log(`Test ${key}: ${finalResult}`);
}

エラー箇所の特定

エラー箇所
...
at JSON.parse (<anonymous>)
...

スタックトレースの最後にある、JSON.parse (<anonymous>)の部分がエラーの発生源とわかります。

エラーの意味

エラー内容
SyntaxError: Unexpected non-whitespace character after JSON at position 1

構文エラー: JSONとして有効な部分を読み取った後、2文字目(インデックス1)に予期しない非空白文字が見つかった

不正な JSON
JSON.parse が JSON の文法に適合しない文字列を受け取った場合、 SyntaxError が発生します。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse より引用

原因

通常のケース
→ JSONの構文ルールに違反している可能性

今回のケース
→ 文字列をJSONとして解釈させようとしたこと(JSON.parse() を使用していること

そもそも、この課題のゴールは文字列を出力することです。

そのために、期待される出力結果(文字列)と実際の処理結果(文字列)を比較して、正しいかどうかをテストしています。

このテストにおいては、期待される出力結果も実際の出力結果も文字列であるため、JSON.parse() を使う必要がなかったということです。

JSON.parse() の処理

JSON.parse()の内部動作(簡易版)
JSON.parse()に文字列が渡されると、パーサは文字列の先頭から1文字ずつ読み込みます。

  • 最初に読み込む文字が、JSONの有効な開始文字({, [, ", 0-9, t, f, n)であるかをチェックします
  • もし有効な文字であれば、パーサはそのデータ型(オブジェクト、配列、文字列、数値など)の解析を開始します
  • 有効な文字でなければ、即座にSyntaxErrorを投げます
    ※ Gemini の解説より引用

逐次、文字を読み取り、ルールに反していないかチェックしているようです。

今回のケースでは、「1」の後に「-」(ハイフン)があり、ハイフンは空白文字ではないため、JSONの構文ルールに違反していると判断し、エラーを投げたと判断できます。

つまり、有効なJSON要素(例: 数値1)の後に続く文字は、構文上許容される文字(数値の残りの部分、カンマ、閉じ括弧、あるいは空白)である必要があります。ハイフンはそれらのどれでもないため、予期せぬ文字として扱われたことを意味します。

有効なJSONの形式

主にオブジェクト配列の形式があります。

1. オブジェクト

オブジェクトは、キーと値のペアの集まりです。
キーは必ずダブルクォーテーション「""」で囲まれた文字列で、コロン「:」の後に値が続きます。
複数のペアはカンマ「,」で区切られます。

{
  "name": "mavo",
  "age": 25,
  "isStudent": false,
  "hobbies": ["reading", "walking"],
  "address": {
    "city": "Tokyo",
    "zipCode": "100-0001"
  }
}

2. 配列

配列の要素には、文字列、数値、オブジェクトなど、あらゆるJSONのデータ型を入れることができます。

[
  "apple",
  "banana",
  {
    "name": "orange",
    "color": "orange"
  },
  123,
  null
]

JSONで値として使えるデータ型

  1. 文字列: ダブルクォーテーション " で囲む(例: "Hello")
  2. 数値: 整数または小数(例: 123)
  3. ブール値: true または false
  4. null
  5. オブジェクト: {} で囲む
  6. 配列: [] で囲む

補足: JSON.stringify()

JSON.stringify() は、JavaScript の値(オブジェクトや配列など)を JSON 形式の文字列に変換する関数です。

例えば、FizzBuzz の結果を配列で保持している場合、そのままでは配列同士の比較が難しいですが、JSON.stringify() を使えば文字列として比較できます

const arr1 = [1, 2, "Fizz"];
const arr2 = [1, 2, "Fizz"];

console.log(JSON.stringify(arr1) === JSON.stringify(arr2)); 
// 出力: true

オブジェクトや配列をそのまま比較したいときには JSON.stringify() が有効です。

一方、今回の課題のように「最終出力が単なる文字列」である場合は、=== による比較で目的を達成できます。

まとめ

この記事で分かったこと

  • 目的に応じた手段を採用できているかチェックすること
  • JSONには有効な形式があること
  • JSON.parse()の内部処理
  • 構文チェックを逐次行なっていること

JSON.parse()は文字列に変換できるものだと勘違いしていたことが、今回のエラーを招いたことがわかりました。

最後までお読みいただき、ありがとうございました。

参考URL

https://recursionist.io/dashboard/problems/418

https://developer.mozilla.org/ja/docs/Learn_web_development/Core/Scripting/JSON

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse

https://qiita.com/shuntaro_tamura/items/5a92c517a7d95d655505

Discussion