「CBORで構造化データを小さく交換する設計|型・tag・決定的encoding・Python」の内容を表す技術イラスト
UNIT 00LAB NOTES FIG.00 · EYECATCH

CBORで構造化データを小さく交換する設計|型・tag・決定的encoding・Python

CBORで構造化dataをbinary交換する設計を解説。型、semantic tag、決定的encoding、重複key拒否、Python検証、hash・署名、DB・API・CAD・IoTへの再利用まで整理します。

DOC NO.EKP-00-1225
DATE
REVB
READ約 14 分
目次

CBORはJSONに近い構造をbinaryで運ぶ

CBOR(Concise Binary Object Representation)は、map、array、整数、文字列、真偽値、NULLなどの構造化dataをbinaryへserializeする形式です。RFC 8949では、小さなmessage、実装codeの小ささ、拡張性が設計目標に挙げられています。

用途は「JSONを圧縮すること」だけではありません。CBORはbyte stringをそのまま保持でき、整数と浮動小数点数を区別し、tagで日時やdecimal fractionなどの意味を付加できます。IoT message、CAD属性、BOM、計測data、API payloadのように、型を保ったままsystem間で受け渡したい場面に向きます。

一方、CBORをdecodeできるだけでは安全な連携になりません。どのkeyとtypeを許すか、tagをどう解釈するか、同じ値を同じbyte列へencodeするか、未知fieldや重複keyをどう扱うかというinterface契約が必要です。

CBORのdata modelを理解する

CBORの先頭byteにはmajor typeと追加情報が入り、後続byteをどう読むかが決まります。RFC 8949が定義する主要な型は次のとおりです。

major typedataの形代表的な用途
0unsigned integer件数、revision、ID
1negative integer符号付きの測定値
2byte stringhash、画像断片、binary ID
3text stringUTF-8の名称、単位、日時文字列
4array座標列、部品一覧
5mapkeyとvalueを持つrecord
6semantic tag日時、bignum、decimalなどの意味付け
7simple value / floattrue、false、null、浮動小数点数

JSON objectに近いものがCBORのmap、JSON arrayに近いものがarrayです。ただし、CBORのmap keyはtextに限定されません。integerやbyte stringもkeyにできます。小さなinteger keyはmessageを短くできますが、1がpart_idを表すといった対応表がなければ読めません。公開APIや長期保存では、可読性のあるtext keyを基本とし、帯域制約が厳しいprofileだけinteger keyを採用する方が保守しやすくなります。

最小recordとschemaを決める

次は、部品属性を表す論理的なrecordです。drawing_digestはJSONではbase64などへ変換が必要ですが、CBORではbyte stringとして直接保持できます。

record = {
    "schema_version": 1,
    "part_id": "P-1001",
    "revision": 7,
    "mass": {
        "coefficient": 24550,
        "exponent": -2,
        "unit": "g",
    },
    "released": True,
    "drawing_digest": bytes.fromhex(
        "7f83b1657ff1fc53b92dc18148a1d65dfa13514a"
        "c2d4d677a6f5d4f4d1d3a7d5"
    ),
    "updated_at": "2026-10-29T04:30:00Z",
}

massは浮動小数点数ではなく、coefficient、exponent、unitを持つmapにしています。値は 24550 × 10^-2 g、つまり245.50 gです。金額、寸法、質量のように小数桁が意味を持つdataでは、binary floatへ無条件に変換せず、decimal表現または整数とscaleを契約します。

最低限、schemaには次を明記します。

  • 必須keyと任意key
  • 各valueのtype、範囲、最大長
  • arrayの要素typeと最大件数
  • unknown keyを許可するか
  • NULLとkey欠落の違い
  • tagの番号、content type、意味
  • schema versionの移行規則
  • decoderのsize、depth、重複keyの制限

CBOR自体はschemaを要求しません。受信側は「CBORとしてvalidか」に加え、「applicationが期待するrecordか」を検証します。

入力から出力までを層に分ける

安全な処理は、decodeと業務validationを一度に済ませません。binaryとして読めるか、CBORとしてvalidか、schemaに適合するか、業務上許可された値かを順に確認します。

flowchart TD
  A[CBOR byte列を受信] --> B[sizeとmedia typeを確認]
  B --> C[厳格な設定でdecode]
  C --> D[schemaと業務ruleを検証]
  D --> E[DB保存またはAPI処理]
  C --> F[decode errorとして拒否]
  D --> G[validation errorとして拒否]

この分離により、破損byte列、重複key、未知tag、必須field不足を別のerrorとして記録できます。logにはpayloadそのものではなく、request ID、error種別、schema version、byte数、必要ならhashだけを残します。

semantic tagは意味を共有する契約

major type 6のtagは、直後のdata itemへ意味を付加します。IANAのCBOR Tags registryには、tag 0の標準日時文字列、tag 1のepoch基準日時、tag 2と3のbignum、tag 4のdecimal fractionなどが登録されています。

tagは単なるlabelではありません。tag 0のcontentはtext string、tag 4のcontentはarrayというように、許される型が決まります。受信側は番号だけでなくcontentの型と値も検証します。未知tagを無視する、generic objectとして渡す、errorにする、という方針もprofileへ明記します。

同じ意味をtag付きとtagなしの両方で許すと、比較や署名が不安定になります。日時fieldについて「tag 0を必須にする」「RFC 3339 textだけを許可しtagは使わない」など、表現を一つに絞ります。相互運用先がJSON中心なら、日時をtext、decimalをcoefficientとexponentのmapにする選択も合理的です。

決定的encodingが必要な場面

同じCBOR data modelでも、整数の長さ、map keyの順序、floatの幅、definite / indefinite lengthなどによって複数のbyte表現が成立します。decode後の値が同じでも、byte列のhashは異なります。

そのため、次の用途では決定的encodingのprofileを固定します。

  • content hashによる重複排除
  • digital signatureやMACの対象化
  • cache keyの生成
  • binary単位の差分比較
  • fixtureを使った再現test

RFC 8949のcore deterministic encodingでは、preferred serializationを使い、indefinite-length itemを使わず、map keyを決定的なbyte順に並べます。ただし、tagの有無、整数とfloatの使い分け、NaN、decimalの複数表現などはapplication側の追加ruleが必要です。

libraryのcanonical optionだけで、別言語の実装と必ず同じになるとは限りません。CBOR profile、tag、number表現、key集合をtest vectorとともに共有します。

Pythonでencode・decode・validationする

次の例はcbor2を使い、決定的にencodeします。受信時はsize、depth、indefinite-length、重複keyを制限し、decode後にschemaを検証します。

from __future__ import annotations

from hashlib import sha256
from typing import Any

import cbor2


MAX_CBOR_BYTES = 64 * 1024
KEYS = {"schema_version", "part_id", "revision", "mass",
        "released", "drawing_digest", "updated_at"}


def validate_part(value: Any) -> dict[str, Any]:
    if not isinstance(value, dict):
        raise ValueError("top-level item must be a map")
    if set(value) != KEYS or value["schema_version"] != 1:
        raise ValueError("invalid keys or schema_version")
    if not isinstance(value["part_id"], str) or not (
        1 <= len(value["part_id"]) <= 64
    ):
        raise ValueError("invalid part_id")
    revision = value["revision"]
    if isinstance(revision, bool) or not isinstance(revision, int):
        raise ValueError("revision must be an integer")
    if not 0 <= revision <= 2_147_483_647:
        raise ValueError("revision out of range")
    if not isinstance(value["released"], bool):
        raise ValueError("released must be boolean")
    digest = value["drawing_digest"]
    if not isinstance(digest, bytes) or len(digest) != 32:
        raise ValueError("drawing_digest must be 32 bytes")
    mass = value["mass"]
    if not isinstance(mass, dict) or set(mass) != {
        "coefficient", "exponent", "unit"}:
        raise ValueError("invalid mass object")
    if any(isinstance(mass[k], bool) or not isinstance(mass[k], int)
           for k in ("coefficient", "exponent")):
        raise ValueError("mass numbers must be integers")
    if not -9 <= mass["exponent"] <= 9 or mass["unit"] != "g":
        raise ValueError("invalid mass")
    if not isinstance(value["updated_at"], str):
        raise ValueError("updated_at must be text")
    return value


def encode_part(record: dict[str, Any]) -> bytes:
    validate_part(dict(record))
    return cbor2.dumps(dict(record), canonical=True)


def decode_part(payload: bytes) -> dict[str, Any]:
    if len(payload) > MAX_CBOR_BYTES:
        raise ValueError("CBOR payload is too large")

    value = cbor2.loads(
        payload,
        max_depth=16,
        allow_indefinite=False,
        allow_duplicate_keys=False,
    )
    return validate_part(value)


encoded = encode_part(record)
decoded = decode_part(encoded)
content_id = sha256(encoded).hexdigest()

boolはPythonではintのsubclassなので、整数検証で先に除外します。hashは決定的にencodeしたbyte列から計算し、再encodeする場合も同じprofileを使います。

正常系と異常系をtestする

inputまたは条件期待する処理
schema version 1のvalid recorddecode後に業務処理へ進む
破損したbyte列CBOR decode errorで拒否
map内の重複keydecode時に拒否
nestingが上限超過resource制限errorで拒否
indefinite-length itemprofile違反として拒否
32 byteでないdigestschema validationで拒否
unknown keyversion policyに従い拒否
unknown semantic tag契約したtag policyで拒否
同じrecordを再encode同じbyte列とhashになる

RFC 8949では、重複keyを持つmapはwell-formedでもvalidではなく、decoderによって最初または最後のvalueだけが残る可能性があります。認証、権限、価格などのfieldで解釈が分かれると危険です。外部入力には、重複keyをerrorにできるdecoder設定を使います。

testでは長すぎる文字列、深いarray、invalid UTF-8、未知tag、非決定的なencodingもfixtureにします。複数言語では共通のhex test vectorを管理します。

version・ID・再実行を設計する

schema_versionはtop-levelへ明示し、受信側が対応できないversionを黙って処理しないようにします。field追加を許す場合でも、security判断に関係するunknown fieldを無視してよいかは別に決めます。

IDはCBORのbinary化と切り離して設計します。部品IDや外部system IDはtext、UUIDを16 byteで持つ場合はbyte orderまで契約します。mapの並び順をrecordの意味に使ってはいけません。

再送の重複判定には、業務IDとrevisionを組み合わせたidempotency keyを使います。決定的encodingはhashを安定させますが、業務上の同一性までは定義しません。

securityと秘密情報の分離

CBORは暗号化形式ではありません。access token、credential、個人情報を入れれば、binary表示で読みにくいだけで内容は保護されません。通信にはTLS、保存には適切なaccess controlや暗号化を使い、署名が必要なら対象byte列とalgorithmを別仕様で定めます。

decoderは攻撃面になります。byte数、nesting depth、array件数、文字列長を制限し、tag 24のような「CBORを含むCBOR」では内側にも同じ制限を適用します。decode結果はplainなmapとarrayとして検証します。

DB・API・CAD・IoTへ再利用する

DBでは検索対象のfieldをcolumnへ展開し、raw payloadは監査や再処理が必要な場合だけ保持します。raw byte列だけでは、検索や参照整合性をDBで確認できません。

APIではContent-Typeを契約し、JSON版とCBOR版で同じ論理schemaを共有するか、CBOR固有のbyte stringやtagを使うかを明記します。JSONへ変換できないtypeを含むなら、debug表示用の診断JSONと正式payloadを区別します。

CADやIoTでは、図面hash、thumbnail、sensor frameなどのbinaryと、part ID、revision、unit、timestampを一つのrecordで運べます。ただし、座標のunit、axis、precision、endianness、revision参照はCBORが自動で決めてくれません。これらをschemaに含めることで、同じdataをDB、API、自動化、設計data資産へ再利用できます。

まとめ

CBORは、JSONに近い構造をcompactなbinaryで交換し、byte stringやsemantic tagを扱える形式です。強みを生かすには、encodeとdecodeだけでなく、key、type、tag、version、size、depth、重複keyの契約が必要です。

hashや署名に使う場合は決定的encodingをprofile化し、numberやtagの表現まで一つに絞ります。受信時は厳格なdecoder設定とapplication schema validationを重ね、CBORのvalidityと業務dataの妥当性を分離します。これにより、IoT、CAD、DB、APIをまたぐ構造化dataを、小ささだけでなく再現性と安全性を保って連携できます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次