できない.dev

オブジェクトをJSON文字列にするには

オブジェクトや構造体を JSON 文字列へ変換する基本形を各言語で示す。
整形の有無と、日本語などの非 ASCII 文字を既定でエスケープするかどうかの違いまでを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

import json
 
user = {"name": "山田太郎", "tags": ["api", "cli"], "active": True}
text = json.dumps(user, ensure_ascii=False, indent=2)
 
print(text)

json.dumps は既定で非 ASCII 文字を \uXXXX へエスケープするため、日本語をそのまま出したいときは ensure_ascii=False を指定する。
indent を渡すと人が読める整形出力になり、省略すれば区切りの詰まった 1 行になる。

JavaScript 実行確認済み

const user = { name: "山田太郎", tags: ["api", "cli"], active: true };
const text = JSON.stringify(user, null, 2);
 
console.log(text);

第 2 引数は出力するキーを絞る replacer、第 3 引数がインデント幅である。
整形が不要なら JSON.stringify(user) だけでよい。
undefined や関数を値に持つプロパティは黙って出力から落ちる点に注意する。

うまくいかない時: React で「Objects are not valid as a React child」が解消できない

TypeScript 実行確認済み

type User = { name: string; tags: string[]; active: boolean };
 
const user: User = { name: "山田太郎", tags: ["api", "cli"], active: true };
const text = JSON.stringify(user, null, 2);
 
console.log(text);

型は「どんな JSON になるか」を保証しない。
JSON.stringify の宣言上の戻り値は string だが、undefined を渡した場合の実際の戻り値は undefined なので、任意の値を受け取る関数では戻り値を string と決めつけない方がよい。

Go 静的確認

package main
 
import (
	"encoding/json"
	"fmt"
)
 
type User struct {
	Name   string   `json:"name"`
	Tags   []string `json:"tags"`
	Active bool     `json:"active"`
}
 
func main() {
	u := User{Name: "山田太郎", Tags: []string{"api", "cli"}, Active: true}
	b, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}

json.Marshal は 1 行、json.MarshalIndent は整形した []byte を返す。
エクスポートされていない小文字始まりのフィールドは出力されないため、JSON 側のキー名は json タグで指定する。
既定では < > & が HTML 用にエスケープされるので、そのまま出したいときは json.Encoder の SetEscapeHTML(false) を使う。

Rust 静的確認

use serde::Serialize;
 
#[derive(Serialize)]
struct User {
    name: String,
    tags: Vec<String>,
    active: bool,
}
 
fn main() -> Result<(), serde_json::Error> {
    let u = User {
        name: "山田太郎".to_string(),
        tags: vec!["api".to_string(), "cli".to_string()],
        active: true,
    };
    println!("{}", serde_json::to_string_pretty(&u)?);
    Ok(())
}

標準ライブラリに JSON は含まれないため、serde と serde_json をクレートとして追加する。
to_string が 1 行、to_string_pretty が整形版で、どちらも Result を返す。
derive(Serialize) を付けていない型は渡した時点でコンパイルエラーになる。

Java 静的確認

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
 
record User(String name, List<String> tags, boolean active) {}
 
public class Main {
    public static void main(String[] args) throws Exception {
        var user = new User("山田太郎", List.of("api", "cli"), true);
        var mapper = new ObjectMapper();
        System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user));
    }
}

JDK の標準ライブラリに JSON は含まれないため、Jackson や Gson を依存に追加する。
writeValueAsString が 1 行の JSON を返し、writerWithDefaultPrettyPrinter() を挟むと整形される。
record はコンポーネント名がそのままキーになる。

C# 静的確認

using System.Text.Json;
using System.Text.Encodings.Web;
 
var user = new User("山田太郎", new[] { "api", "cli" }, true);
var options = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
Console.WriteLine(JsonSerializer.Serialize(user, options));
 
record User(string Name, string[] Tags, bool Active);

System.Text.Json の既定のエンコーダは非 ASCII 文字を \uXXXX へ逃がすため、日本語をそのまま出したいときは UnsafeRelaxedJsonEscaping を指定する。
WriteIndented を true にすると整形出力になり、キー名を camelCase にしたいときは PropertyNamingPolicy を設定する。

つまずき

同じデータでも、非 ASCII 文字を \uXXXX へ逃がすかどうかは言語ごとに既定が違う。
Python の json.dumps と .NET の JsonSerializer は既定でエスケープし、JavaScript の JSON.stringify はエスケープしない。
どちらも JSON としては正しいため、スナップショットテストや差分比較で食い違ったときは、まずこの既定値の違いを疑うとよい。

循環参照は文字列化できない

自分自身を含むオブジェクトは JSON にできず、実行時エラーになる。
Node.js では TypeError: Converting circular structure to JSON、Python では ValueError: Circular reference detected が送出される(いずれも本記事の作成時に手元で発生を確認した)。
親子を相互参照させたデータ構造をそのまま渡したときに起きるので、出力用の形へ詰め替えてから文字列化する。