Skip to content

Commit 63e731f

Browse files
authored
JSON to CBOR converter (#34)
1 parent bd36754 commit 63e731f

9 files changed

Lines changed: 386 additions & 18 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ jobs:
1616
test-command: |
1717
nimble dump --solve
1818
nimble list --installed --version
19-
env nimble features
2019
env NIMLANG=c nimble test
2120
env NIMLANG=cpp nimble test
21+
env nimble --useSystemNim install -y --depsonly json_serialization bigints
2222
env nimble examples
23+
env NIMLANG=c nimble test_dev
24+
env NIMLANG=cpp nimble test_dev

cbor_serialization.nimble

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,17 @@ skipDirs = @["tests", "fuzzer"]
1919
requires "nim >= 2.0.0",
2020
"serialization >= 0.4.9", "stew >= 0.4.1", "npeg >= 1.3.0", "results", "unittest2"
2121

22-
#feature "bigints":
23-
# require "bigints"
22+
#feature "dev":
23+
# requires "bigints", "json_serialization"
24+
25+
from os import quoteShell
26+
from strutils import endsWith
2427

2528
let nimc = getEnv("NIMC", "nim") # Which nim compiler to use
2629
let lang = getEnv("NIMLANG", "c") # Which backend (c/cpp/js)
2730
let flags = getEnv("NIMFLAGS", "") # Extra flags for the compiler
2831
let verbose = getEnv("V", "") notin ["", "0"]
2932

30-
from os import quoteShell
31-
from strutils import endsWith
32-
3333
let cfg =
3434
" --styleCheck:usages --styleCheck:error" &
3535
(if verbose: "" else: " --verbosity:0 --hints:off") & " --outdir:build " &
@@ -47,6 +47,10 @@ task test, "Run all tests":
4747
for threads in ["--threads:off", "--threads:on"]:
4848
run threads, "tests/test_all"
4949

50+
task test_dev, "Run all dev tests":
51+
for threads in ["--threads:off", "--threads:on"]:
52+
run threads, "tests/test_all_dev"
53+
5054
task examples, "Run examples":
5155
# Run book examples
5256
for file in listFiles("docs/examples"):
@@ -57,6 +61,3 @@ task docs, "Generate API documentation":
5761
exec "mdbook build docs"
5862
exec nimc & " doc " &
5963
"--git.url:https://github.com/vacp2p/nim-cbor-serialization --git.commit:master --outdir:docs/book/api --project cbor_serialization"
60-
61-
task features, "Install all features":
62-
exec "nimble install bigints -y"

cbor_serialization/json_utils.nim

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# cbor-serialization
2+
# Copyright (c) 2026 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5+
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
6+
# at your option.
7+
# This file may not be copied, modified, or distributed except according to
8+
# those terms.
9+
10+
{.push raises: [], gcsafe.}
11+
12+
import
13+
std/[math, strutils], stew/[base64, byteutils], json_serialization, ./[reader, writer]
14+
15+
export JsonString, CborBytes, SerializationError
16+
17+
# https://www.rfc-editor.org/rfc/rfc8949.html#section-6.1
18+
proc writeToJson*(
19+
reader: var CborReader, writer: var JsonWriter, substitute = true
20+
) {.raises: [IOError, SerializationError].} =
21+
mixin writeValue, readValue
22+
template p(): untyped =
23+
reader.parser
24+
25+
template substituteOrRaise(msg: string): untyped =
26+
if substitute:
27+
writer.writeValue JsonString("null")
28+
else:
29+
p.raiseUnexpectedValue(msg)
30+
31+
template readAsJson(reader: untyped): string =
32+
var outTmp = memoryOutput()
33+
var writerTmp = JsonWriter.init(outTmp)
34+
writeToJson(reader, writerTmp)
35+
outTmp.getOutput(string)
36+
37+
case p.cborKind()
38+
of CborValueKind.Bytes:
39+
writer.writeValue(Base64Url.encode(reader.readValue(seq[byte])))
40+
of CborValueKind.String:
41+
writer.writeValue(reader.readValue(string))
42+
of CborValueKind.Unsigned:
43+
writer.writeValue(reader.readValue(uint64))
44+
of CborValueKind.Negative:
45+
writer.writeValue(reader.readValue(int64))
46+
of CborValueKind.Float:
47+
let f = reader.readValue(float64)
48+
case f.classify
49+
of fcNan:
50+
substituteOrRaise("Nan float")
51+
of fcInf:
52+
substituteOrRaise("Positive infinity float")
53+
of fcNegInf:
54+
substituteOrRaise("Negative infinity float")
55+
else:
56+
writer.writeValue(f)
57+
of CborValueKind.Object:
58+
writeObject(writer):
59+
parseObjectCustomKey(reader):
60+
let key =
61+
case p.cborKind()
62+
of CborValueKind.String:
63+
reader.readValue(string)
64+
of CborValueKind.Tag:
65+
var val: string
66+
var tag: uint64
67+
parseTag(reader, tag):
68+
val =
69+
case p.cborKind()
70+
of CborValueKind.String:
71+
reader.readValue(string)
72+
else:
73+
reader.readAsJson()
74+
val
75+
else:
76+
reader.readAsJson()
77+
writer.writeName(key)
78+
do:
79+
writeToJson(reader, writer)
80+
of CborValueKind.Array:
81+
writeArray(writer):
82+
parseArray(reader):
83+
writeToJson(reader, writer)
84+
of CborValueKind.Tag:
85+
var tag: uint64
86+
parseTag(reader, tag):
87+
case tag
88+
of 2, 21:
89+
writer.writeValue(Base64Url.encode(reader.readValue(seq[byte])))
90+
of 3:
91+
writer.writeValue("~" & Base64Url.encode(reader.readValue(seq[byte])))
92+
of 22:
93+
writer.writeValue(Base64.encode(reader.readValue(seq[byte])))
94+
of 23:
95+
writer.writeValue("0x" & toUpperAscii(toHex(reader.readValue(seq[byte]))))
96+
else:
97+
writeToJson(reader, writer)
98+
of CborValueKind.Simple:
99+
let val = reader.readValue(CborSimpleValue)
100+
substituteOrRaise($val)
101+
of CborValueKind.Bool:
102+
writer.writeValue(reader.readValue(bool))
103+
of CborValueKind.Null:
104+
let _ = reader.readValue(CborSimpleValue)
105+
writer.writeValue JsonString("null")
106+
of CborValueKind.Undefined:
107+
let _ = reader.readValue(CborSimpleValue)
108+
substituteOrRaise("undefined")
109+
110+
proc toJson*(
111+
cbor: CborBytes, substitute = true, pretty = false
112+
): string {.raises: [SerializationError].} =
113+
var cborStream = unsafeMemoryInput(seq[byte](cbor))
114+
var reader = CborReader[DefaultFlavor].init(cborStream)
115+
var jsonStream = memoryOutput()
116+
var writer = JsonWriter[DefaultFlavor].init(jsonStream, pretty)
117+
try:
118+
reader.writeToJson(writer, substitute)
119+
except IOError:
120+
raiseAssert "memoryOutput is exception-free"
121+
jsonStream.getOutput(string)
122+
123+
# https://www.rfc-editor.org/rfc/rfc8949.html#section-6.2
124+
proc writeToCbor*(
125+
reader: var JsonReader, writer: var CborWriter
126+
) {.raises: [IOError, SerializationError].} =
127+
mixin writeValue, readValue
128+
129+
case reader.tokKind
130+
of JsonValueKind.String:
131+
writer.writeValue(reader.readValue(string))
132+
of JsonValueKind.Number:
133+
let num = reader.readValue(JsonNumber[uint64])
134+
if num.isFloat():
135+
writer.writeValue(reader.toFloat(num, float64))
136+
elif num.sign == JsonSign.Neg:
137+
writer.writeValue(reader.toInt(num, int64, portable = false))
138+
else:
139+
writer.writeValue(reader.toInt(num, uint64, portable = false))
140+
of JsonValueKind.Object:
141+
writeObject(writer):
142+
parseObjectWithoutSkip(reader, key):
143+
writer.writeName(key)
144+
writeToCbor(reader, writer)
145+
of JsonValueKind.Array:
146+
writeArray(writer):
147+
parseArray(reader):
148+
writeToCbor(reader, writer)
149+
of JsonValueKind.Bool:
150+
writer.writeValue(reader.readValue(bool))
151+
of JsonValueKind.Null:
152+
reader.parseNull()
153+
writer.writeValue(cborNull)
154+
155+
proc toCbor*(
156+
json: JsonString, definiteLen = false
157+
): seq[byte] {.raises: [SerializationError].} =
158+
var jsonStream = unsafeMemoryInput(string(json))
159+
var reader = JsonReader[DefaultFlavor].init(jsonStream)
160+
var cborStream = memoryOutput()
161+
var writer = CborWriter[DefaultFlavor].init(cborStream)
162+
try:
163+
reader.writeToCbor(writer)
164+
except IOError:
165+
raiseAssert "memoryOutput is exception-free"
166+
if definiteLen:
167+
# XXX writeToCbor writes map/array as streams (indefinite length);
168+
# decode-encode dance will convert indefinite to definite length;
169+
# implement definiteLen mode in writeObject/Array to avoid this
170+
Cbor.encode(Cbor.decode(cborStream.getOutput(seq[byte]), CborValueRef))
171+
else:
172+
cborStream.getOutput(seq[byte])

docs/examples/json0.nim

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{.push gcsafe, raises: [].}
2+
3+
# ANCHOR: Import
4+
import cbor_serialization, cbor_serialization/json_utils
5+
# ANCHOR_END: Import
6+
7+
# ANCHOR: json
8+
let jsonDecoded = """{"cborrpc":"2.0","method":"subtract","params":[42,3],"id":1}"""
9+
# ANCHOR_END: json
10+
11+
# ANCHOR: toCbor
12+
let encoded = toCbor(jsonDecoded.JsonString)
13+
# ANCHOR_END: toCbor
14+
15+
# ANCHOR: toJson
16+
doAssert toJson(encoded.CborBytes) == jsonDecoded
17+
# ANCHOR_END: toJson

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [Streaming](./streaming.md)
77
- [CDDL](./cddl.md)
88
- [Debugging](./debugging.md)
9+
- [JSON](./json.md)
910
- [Reference](./reference.md)
1011

1112
# Developer guide

docs/src/json.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# JSON
2+
3+
This section provides an overview of the `cbor_serialization` json tools.
4+
5+
## JSON to CBOR
6+
7+
JSON can be converted to CBOR with the `toCbor(JsonString): seq[byte]` API.
8+
The implementation converts JSON as described in [RFC8949](https://www.rfc-editor.org/rfc/rfc8949.html#section-6.2).
9+
10+
Requires the `json_serialization` nimble package.
11+
12+
Import `json_utils`:
13+
14+
```nim
15+
{{#include ../examples/json0.nim:Import}}
16+
```
17+
18+
Encode a JSON string to CBOR bytes:
19+
20+
```nim
21+
{{#include ../examples/json0.nim:json}}
22+
```
23+
24+
```nim
25+
{{#include ../examples/json0.nim:toCbor}}
26+
```
27+
28+
## CBOR to JSON
29+
30+
CBOR can be converted to JSON with the `toJson(CborBytes): string` API.
31+
The implementation converts CBOR as described in [RFC8949](https://www.rfc-editor.org/rfc/rfc8949.html#section-6.1).
32+
33+
Note some CBOR types do not have direct analogs in JSON:
34+
35+
- NaN, -Infinity, Infinity, undefined, and simple values are converted to null.
36+
- `seq[byte]` is base64 encoded.
37+
- Tags 2, 21 and 22 are base64 encoded.
38+
- Tag 3 is base64 encoded and prefixed with `~`.
39+
- Tag 23 is hex encoded.
40+
- All tag numbers are lost (not encoded); only their value is encoded.
41+
- Non-string map keys are stringified using `toJson` which can create key collisions.
42+
43+
Requires the `json_serialization` nimble package.
44+
45+
Import `json_utils`:
46+
47+
```nim
48+
{{#include ../examples/json0.nim:Import}}
49+
```
50+
51+
Encode CBOR bytes to a JSON string:
52+
53+
```nim
54+
{{#include ../examples/json0.nim:toJson}}
55+
```

tests/test_all.nim

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,8 @@
1010
{.warning[UnusedImport]: off.}
1111

1212
import
13-
test_spec, test_serialization, test_simple_value, test_cbor_flavor, test_parser,
14-
test_reader, test_writer, test_valueref, test_cbor_raw, test_malformed, test_std,
15-
test_overloads, test_edn, test_cddl_parser, test_cddl_gen
16-
17-
template importBigints() =
18-
import bigints
19-
20-
when compiles(importBigints):
21-
import test_bigints
13+
./[
14+
test_spec, test_serialization, test_simple_value, test_cbor_flavor, test_parser,
15+
test_reader, test_writer, test_valueref, test_cbor_raw, test_malformed, test_std,
16+
test_overloads, test_edn, test_cddl_parser, test_cddl_gen,
17+
]

tests/test_all_dev.nim

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# cbor-serialization
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5+
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
6+
# at your option.
7+
# This file may not be copied, modified, or distributed except according to
8+
# those terms.
9+
10+
{.warning[UnusedImport]: off.}
11+
12+
import ./[test_bigints, test_json_utils]

0 commit comments

Comments
 (0)