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 type | dataの形 | 代表的な用途 |
|---|---|---|
| 0 | unsigned integer | 件数、revision、ID |
| 1 | negative integer | 符号付きの測定値 |
| 2 | byte string | hash、画像断片、binary ID |
| 3 | text string | UTF-8の名称、単位、日時文字列 |
| 4 | array | 座標列、部品一覧 |
| 5 | map | keyとvalueを持つrecord |
| 6 | semantic tag | 日時、bignum、decimalなどの意味付け |
| 7 | simple value / float | true、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 record | decode後に業務処理へ進む |
| 破損したbyte列 | CBOR decode errorで拒否 |
| map内の重複key | decode時に拒否 |
| nestingが上限超過 | resource制限errorで拒否 |
| indefinite-length item | profile違反として拒否 |
| 32 byteでないdigest | schema validationで拒否 |
| unknown key | version 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を、小ささだけでなく再現性と安全性を保って連携できます。
