Node.jsの組み込みモジュールutilを使ってオブジェクトを文字列に変換する

Node.jsを使っているとき、ログ出力やデバッグのためにオブジェクトを文字列に変換したいことがあります。今回、組み込みモジュールであるutilを使うことで、ちょっと強引ながらオブジェクトを文字列に変える方法を知ったので書き残しておきます。

この記事で紹介している実装はGitHubにおいてあります。 ソースコード全体を確認したい場合や実際に動かした場合は参考にしてください。

JavaScriptでオブジェクトを文字列にするときの問題点

JavaではtoStringメソッドがオーバーライドされていることが多く、欲しい情報が含まれた文字列にすることができます。しかし、Node.js(JavaScript)ではtoStringメソッドがオーバーライド実装されていることは稀で、[object Object]といった意味のない文字列が返ってきます。

他には、JSON.stringify()を使うことで文字列にできますが、エラーが発生するケースがあります。 下の実装のように循環している場合、TypeErrorがスローされます。

const obj = {
  message: 'this is object',
};

obj.myself = obj;
console.log(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON

utilモジュールを使うとどうなるか

utilモジュールは先程の問題を解決してくれます。 また、utilモジュールはconsole.logでのオブジェクトの文字列化にも使われています。

この段落ではconsole.logとutilモジュールの2つのメソッドといくつかのオプションを使った文字列化を見ていきます。

今回、文字列にするオブジェクトは以下のとおりです。

const obj = {
    depth1: {
        depth2: {
            depth3: {
                depth4: {
                    depth5: {
                        depth6: 'deep',
                    },
                },
            },
        },
    },
    string: 'this is string',
    longString: "I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes.",
    number: 123456,
    array: [
        1, 2, 3, 4,
    ],
    error: new Error('test error'),
};
obj.myself = obj;

このオブジェクトの特徴としては、 - 最大で6つの階層があり、 - 長い文字列を値とするプロパティ、 - 数値を値とするプロパティ、 - 配列を値とするプロパティ、 - Errorオブジェクトを値とするプロパティ、 - 自分自身を参照した循環しているプロパティ、 が存在しています。

console.log

まず、参考としてutilモジュールを内部で使っていると言われているconsole.logによる文字列化を見ていきます。

console.log(obj);

結果:

オブジェクトの階層は3つまでは表示され、それ以上は[Object]にされてしまいました。循環しているプロパティは[Circular *1]と表示されました。

<ref *1> {
  depth1: { depth2: { depth3: [Object] } },
  string: 'this is string',
  longString: "I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes.",
  number: 123456,
  array: [ 1, 2, 3, 4 ],
  error: Error: test error
      at Object.<anonymous> (C:\mypath\nodejs-module-labo\util\index.js:22:12)
      at Module._compile (internal/modules/cjs/loader.js:1063:30)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
      at Module.load (internal/modules/cjs/loader.js:928:32)
      at Function.Module._load (internal/modules/cjs/loader.js:769:14)
      at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
      at internal/main/run_main_module.js:17:47,
  myself: [Circular *1]
}

util.format

では、utilモジュールのformatメソッドを使ってみます。 formatメソッドはprintfのようにフォーマット指定子によって用意したパターンに値を渡すことで、オブジェクトを文字列にすることができます。

実装は以下のようになります。

const util = require('util');
// import * as util from 'util';

console.log(util.format('%o', obj));

結果:

オブジェクトの階層は5つまでは表示されるようになりました。それ以上はconsole.logと同じように[Object]となっています。循環しているプロパティは同じく[Circular *1]と表示されました。 配列では配列の長さが追加表示されています。Errorオブジェクトではstackとmessageが追加で表示されるようになりました。

配列とErrorオブジェクトの文字列化については冗長に見えます。

<ref *1> {
  depth1: {
    depth2: { depth3: { depth4: { depth5: [Object] } } }
  },
  string: 'this is string',
  longString: "I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes.",
  number: 123456,
  array: [ 1, 2, 3, 4, [length]: 4 ],
  error: Error: test error
      at Object.<anonymous> (C:\mypath\nodejs-module-labo\util\index.js:22:12)
      at Module._compile (internal/modules/cjs/loader.js:1063:30)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
      at Module.load (internal/modules/cjs/loader.js:928:32)
      at Function.Module._load (internal/modules/cjs/loader.js:769:14)
      at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
      at internal/main/run_main_module.js:17:47 {
    [stack]: 'Error: test error\n' +
      '    at Object.<anonymous> (D:\\DocumentsD\\IT\\gitrepo\\nodejs-module-labo\\util\\index.js:22:12)\n' +
      '    at Module._compile (internal/modules/cjs/loader.js:1063:30)\n' +
      '    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)\n' +
      '    at Module.load (internal/modules/cjs/loader.js:928:32)\n' +
      '    at Function.Module._load (internal/modules/cjs/loader.js:769:14)\n' +
      '    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)\n' +
      '    at internal/main/run_main_module.js:17:47',
    [message]: 'test error'
  },
  myself: [Circular *1]
}

util.inspect

オブジェクトを検証するutilモジュールのinspectメソッドを使ってみます。 おそらくこのinspectメソッドがconsole.logの内部実装に使われているはずです。

const util = require('util');
// import * as util from 'util';

console.log(util.inspect(obj));

結果:

console.logと同じ結果が得られました。

<ref *1> {
  depth1: { depth2: { depth3: [Object] } },
  string: 'this is string',
  longString: "I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes.",
  number: 123456,
  array: [ 1, 2, 3, 4 ],
  error: Error: test error
      at Object.<anonymous> (C:\mypath\nodejs-module-labo\util\index.js:22:12)
      at Module._compile (internal/modules/cjs/loader.js:1063:30)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
      at Module.load (internal/modules/cjs/loader.js:928:32)
      at Function.Module._load (internal/modules/cjs/loader.js:769:14)
      at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
      at internal/main/run_main_module.js:17:47,
  myself: [Circular *1]
}

(おすすめ)util.inspectでオプションを使う

util.inpsectは第2引数にオプションを渡すことができ、文字列化の設定を変更することができます。 このやり方が今回記事で紹介しているオブジェクトの文字列化の方法としては一番良いと思っています。

今回はオプションを使って、 - これまで階層が3つもしくは5つまでしか表示されなかったのを全て表示されるように変更し、 - プロパティごとの改行をなくし、 - 1行あたりの表示文字数の制限をなくして 表示がコンパクトになるようにしてみました。

これによって、とりあえず行数をあまり使わずに情報を全部表示する、ということができます。

const util = require('util');
// import * as util from 'util';

console.log(util.inspect(obj, {
  depth: Infinity,
  breakLength: Infinity,
  compact: true,
}));

結果:

階層が5以上になっても省略されることがなく、プロパティごとの改行もなくなりました。 ただし、Errorオブジェクトのスタックトレースはオプションを無視して改行されました。

<ref *1> { depth1: { depth2: { depth3: { depth4: { depth5: { depth6: 'deep' } } } } }, string: 'this is string', longString: "I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes. I thought what I'd do was, I'd pretend I was one of those deaf-mutes.", number: 123456, array: [ 1, 2, 3, 4 ], error: Error: test error
       at Object.<anonymous> (C:\mypath\nodejs-module-labo\util\index.js:22:12)
       at Module._compile (internal/modules/cjs/loader.js:1063:30)
       at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
       at Module.load (internal/modules/cjs/loader.js:928:32)
       at Function.Module._load (internal/modules/cjs/loader.js:769:14)
       at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
       at internal/main/run_main_module.js:17:47, myself: [Circular *1] }

(おまけ)JSON.stringify

おまけとしてJSON.stringifyを使ってエラーが投げられるパターンを載せておきます。

console.log(JSON.stringify(obj));

結果:

C:\mypath\nodejs-module-labo\util\index.js:52
console.log(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON
                 ^

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'myself' closes the circle
    at JSON.stringify (<anonymous>)
    at Object.<anonymous> (C:\mypath\nodejs-module-labo\util\index.js:52:18)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_module.js:17:47

utilモジュールのドキュメントの日本語訳

最後にutil.formatとutil.inspectのドキュメントの日本語訳を載せておきます。 この記事で使った実装以外を試すとき等に使ってもらえれば幸いです。

util.format(format[, ...args])

参考:https://nodejs.org/docs/latest-v18.x/api/util.html#utilformatformat-args

引数

  • format: 文字列型。printfのようなフォーマット文字列。
  • args: any型。文字列として表示する値。

戻り値

  • 文字列型。フォーマットされた文字列。

util.format()メソッドは、最初の引数をゼロ個以上のフォーマット指定子を含んだprintfのようなフォーマット文字列として利用し、フォーマットされた文字列を返します。それぞれの指定子は対応する引数から変換された値に置換されます。

  • %o: Object。一般的なJavaScriptオブジェクトフォーマッティングによるオブジェクトの文字列表現。util.inspect(){ showHidden: true, showProxy: true }オプションで利用したものと似ています。これは非列挙可能型のプロパティとプロキシを含む全オブジェクトを表示するでしょう。

util.inspect

https://nodejs.org/docs/latest-v18.x/api/util.html#utilinspectobject-options https://nodejs.org/docs/latest-v18.x/api/util.html#utilinspectobject-showhidden-depth-colors

  • util.inspect(object[, options])
  • util.inspect(object[, showHidden[, depth[, colors]]])

2種類の呼び出しがあり、1つ目のoptionsを使うほうで2つ目の呼び出しもカバーされています。

引数

  • object: any型。JavaScriptのプリミティブ型またはObject
  • options: Object型。
    • showHidden: boolean型。表示するシンボルおよびプロパティを決定する。trueを指定すると、列挙不可能なシンボルおよびプロパティも表示する。デフォルトはfalse
    • depth: 数値型。引数object再帰的に表示するときに展開する階層数。全部展開するならばInfinityまたはnullを指定する。デフォルトは2
    • colors: boolean型。出力にANSIカラーコード形式で色がつく。デフォルトはfalse
    • customInspect: boolean型。util.inspect.customプロパティには関数を設定することができ、表示をカスタマイズすることができるが、その関数を呼び出すか否かを決定できる。falseの場合、呼び出さない。デフォルトはtrue
    • showProxy: boolean型。JavaScriptProxyオブジェクトを表示するとき、targethandlerも含めて表示するかどうかを決める。trueを指定すると表示する。デフォルトはfalse
    • maxArrayLength: integer型。配列を表示するさいの最大の要素数。配列が指定された数よりも多い要素数をもつ場合、その要素は表示されない。全て表示するにはInfinityまたはnullを指定し、0または負の数を指定した場合、全て表示されない。デフォルトは100
    • maxStringLength: integer型。表示する最大の文字数。全て表示するにはInfinityまたはnullを指定し、0または負の数を指定した場合、全て表示されない。デフォルトは10000
    • breakLength: integer型。改行する文字数を指定する(後述のbreakLengthとも連携)。すべて1行で表示するにはInfinityを指定し、後述のcompacttrueまたは1以上の数値を設定する。デフォルトは80
    • compact: boolean型またはinteger型。表示をコンパクトにするかどうか、正確には表示するオブジェクトのプロパティごとに改行するか、指定された数だけを1行にして表示するかを決める(ただし、1行に表示する文字数はbreakLengthによっても制御される)。falseを指定するとプロパティごとに改行する。数値が指定された場合、その数のプロパティを表示するまでに1行の文字数が前述のbreakLengthの値を超えなければ改行せずに表示する。デフォルトは3
    • sorted: boolean型またはFunction型。プロパティ、エントリのソート順を決定する。trueまたはFunction型を指定した場合、オブジェクトの全プロパティ、SetおよびMapの全エントリをソートする。trueであればJavaScriptのデフォルトソート、Function型であればそれを使ってソートする。
    • getters: boolean型または文字列型。ゲッター・セッターの表示を決定する。trueが指定されるとゲッターを表示する。getを指定するとセッターに対応していないゲッターだけを表示する。setを指定するとセッターに対応しているゲッターだけを表示する。デフォルトはfalse
    • numericSeparator: boolean型。数値の表示形式を指定する。trueを指定すると、すべてのbigintとnumberを3桁ごとにアンダースコア(_)で区切る。デフォルトはfalse

戻り値 - 文字列型。objectの文字列表現。

参考情報


追記

オブジェクトを循環参照させたときに、無限ループにならないことを確認したので記事にしました。

s1r-j.hatenablog.com