はじめに
Upstash Redisに保存したデータを取り出して JSON.parse() したところ、次のエラーが出ました。
SyntaxError: "[object Object]" is not valid JSON
保存するときに JSON.stringify() したので、取り出すときに JSON.parse() するのは当然だと思っていました。しかし、Upstash Redisではこれがエラーの原因になります。
この記事では、エラーの原因と解決方法をまとめます。
問題
Next.jsでWeb Push通知を作っていて、通知の送り先の情報(購読情報)をUpstash Redisに保存していました。
// 購読情報(オブジェクト)を文字列にしてRedisに保存する
await redis.set("push-subscription", JSON.stringify(subscription));
// Redisから購読情報を取り出す(文字列が返ってくると思っていた)
const subscriptionJson = (await redis.get("push-subscription")) as string | null;
if (!subscriptionJson) {
return NextResponse.json({ ok: false, reason: "no subscription" });
}
// 文字列をオブジェクトに戻してから、通知を送る
await webPush.sendNotification(
JSON.parse(subscriptionJson),
JSON.stringify({ title: "通知タイトル", body: "本文" }),
);
通知が届かなかったので、Vercelのログを確認すると次のエラーが出ていました。
[error] Failed to send push notification: SyntaxError: "[object Object]" is not valid JSON
at JSON.parse (<anonymous>)
エラーの意味
エラーは「JSON.parse()に "[object Object]" という文字列が渡されて、JSONとして読めなかった」という意味です。
では、"[object Object]" という文字列はどこから来たのでしょうか。これは、オブジェクトを文字列に変換したときに出てくる文字列です。
String({ name: "taro" }); // → "[object Object]"
JSON.parse() は、受け取った値が文字列でなければ、まず文字列に変換してから読み込みます。つまり、オブジェクトを渡すと次のように動きます。
const obj = { name: "taro" };
JSON.parse(obj);
// ① オブジェクトを文字列に変換する → "[object Object]"
// ② "[object Object]" をJSONとして読もうとする → 読めないのでエラー
ここから、JSON.parse() に渡した subscriptionJson は文字列ではなく、すでにオブジェクトだったことがわかります。
解決方法
原因:Upstash Redisが自動でオブジェクトに戻していた
@upstash/redis には、保存するときと取り出すときに、値を自動で変換する仕組みがあります。
| タイミング | Upstashが自動でやってくれること |
|---|---|
| 保存(redis.set) | オブジェクトを渡すと、JSON.stringify() して文字列で保存する |
| 取り出し(redis.get) | JSONとして読める文字列なら、JSON.parse() してオブジェクトで返す |
今回のコードの流れを図にすると、次のようになります。
【保存】
【取り出し】
Upstashがすでにオブジェクトに戻してくれていたのに、自分でもう一度 JSON.parse() していたため、エラーになっていました。
対応:JSON.parse() を削除する
自分で JSON.parse() を呼ぶのをやめて、redis.get() で取り出したオブジェクトをそのまま使うよう修正しました。
// Redisから購読情報を取り出す
// Upstashが自動でオブジェクトに戻してくれるので、JSON.parse() は不要
const subscription = await redis.get<webPush.PushSubscription>("push-subscription");
if (!subscription) {
return NextResponse.json({ ok: false, reason: "no subscription" });
}
// 取り出したオブジェクトをそのまま渡す
await webPush.sendNotification(
subscription,
JSON.stringify({ title: "通知タイトル", body: "本文" }),
);
redis.get<webPush.PushSubscription>(...) のように < > で型を書くと、「取り出す値はこの型です」とTypeScriptに伝えられます。エディタの補完も効くようになります。
なぜTypeScriptはエラーに気づけなかったのか
修正前のコードでは、取り出した値に as string | null と書いていました。
const subscriptionJson = (await redis.get("push-subscription")) as string | null;
as は「この値はこの型だから、そのつもりで扱って」とTypeScriptに言い切る書き方です。TypeScriptは中身を確かめずに信じるので、実際の値がオブジェクトでも、文字列として扱ってしまいます。
その結果、型チェックでは何のエラーも出ず、実行して初めてエラーになりました。as は型チェックをすり抜ける書き方なので、本当にその型になっているかを確かめてから使う必要があります。
まとめ
-
@upstash/redisのredis.get()は、JSONとして読める文字列を自動でオブジェクトに戻して返す - そのため、自分で
JSON.parse()を呼ぶとエラーになる。JSON.parse()は削除する -
"[object Object]" is not valid JSONは、「JSON.parse()にオブジェクトを渡している」というサイン -
asで型を言い切ると、型チェックで間違いに気づけなくなる
おわりに
「保存するときに JSON.stringify() したら、取り出すときに JSON.parse() する」という思い込みが原因でした。
また、as で型を言い切ってしまったため、型チェックで間違いに気づけず、エラーの発見が遅れました。これからは as をできるだけ使わず、< > で型を渡すなど、TypeScriptに確かめてもらえる書き方をしていきたいと思います。
使っているライブラリが裏側でどんな変換をしているかを知っておくことも大事だと感じました。
参考
