Liking cljdoc? Tell your friends :D

clj-protobuf

The protobuf runtime for code generated by protoc-gen-clojure: records in, protoc's bytes out. Editions supported through 2024.

;; deps.edn
com.github.bpalermo/clj-protobuf {:mvn/version "0.1.5"}

What it is

protoc-gen-clojure emits a defrecord per message plus X->proto / proto->X conversion fns; this library is everything those fns call — the descriptor machinery, the field codec, and byte-level encode/decode:

(require '[clj-protobuf.core :as pb]
         '[acme.greeter.greeter :as g])   ; generated

(-> (g/map->HelloRequest {:name "world" :repeat-count 2})
    g/HelloRequest->proto
    pb/encode)                            ; -> bytes, byte-identical to protoc's

(->> bytes
     (pb/decode g/HelloRequest-prototype)
     g/proto->HelloRequest)               ; -> record; absent fields are nil

Records and plain maps are interchangeable everywhere a message value goes.

Semantics worth knowing

  • nil means absent, both directions. Setting nil sets nothing; reading an unset explicit-presence field returns nil. Fields with IMPLICIT presence (editions) and proto3 no-label scalars have no absence — they read back as their value, default included.
  • The proto field name is the authority. Kebab-cased keys are derived from it (camelCaseField:camel-case-field); STYLE_LEGACY files work because nothing ever converts backwards.
  • Enums are keywords of the exact proto value name (:COLOR_RED), lossless in both directions; numbers, strings and EnumValueDescriptors are accepted on the way in.
  • Editions are the descriptor's problem. The generated file embeds its FileDescriptorProto; protobuf-java resolves presence, DELIMITED encoding and utf8 validation when the descriptor builds. This library carries no edition-specific code, which is why new editions need a protobuf-java bump and nothing else.
  • Unknown fields survive on parsed Messages but are necessarily dropped by a record round trip; pb/unknown-fields inspects them when it matters.

Performance

Generated code carries a Java-class hint per message. When the matching java_proto_library classes are on the classpath the prototypes silently switch from DynamicMessage to the generated classes, with byte-for-byte identical output (the byte-identity suite proves both arms against protoc's own Java backend).

Measured with bazel run //bench:run -- quick (criterium; JDK 21, Linux x86_64; mean latency / allocated bytes per op; full Clojure-data-to-bytes pipelines). java is protoc's generated builders driven directly; jsonista and data.json carry the same value as JSON:

Encode (Clojure data → bytes)

shapejavahinteddynamicjsonistadata.json
tiny56 ns / 56 B200 ns / 136 B486 ns / 448 B367 ns / 608 B884 ns / 624 B
flat609 ns / 400 B1.16 µs / 488 B1.74 µs / 880 B1.32 µs / 1248 B3.16 µs / 2208 B
deep1.35 µs / 744 B2.11 µs / 1712 B738 ns / 1032 B2.35 µs / 1392 B
wide-repeated4.36 µs / 2944 B4.30 µs / 3152 B2.74 µs / 1096 B6.67 µs / 4216 B
repeated-messages2.57 µs / 2360 B11.22 µs / 4440 B15.80 µs / 9840 B3.84 µs / 4024 B19.01 µs / 10424 B
map-heavy12.17 µs / 13744 B35.42 µs / 25504 B6.12 µs / 3616 B13.64 µs / 10720 B

Decode (bytes → Clojure data)

shapejavahinteddynamicjsonistadata.json
tiny94 ns / 192 B239 ns / 232 B590 ns / 608 B678 ns / 1176 B591 ns / 1584 B
flat346 ns / 432 B1.18 µs / 600 B1.95 µs / 1096 B2.07 µs / 2248 B3.27 µs / 5744 B
deep1.54 µs / 1288 B2.79 µs / 2480 B1.42 µs / 2208 B1.45 µs / 3840 B
wide-repeated3.53 µs / 4344 B6.90 µs / 4712 B3.79 µs / 4144 B3.06 µs / 11496 B
repeated-messages2.07 µs / 3328 B12.00 µs / 8448 B18.25 µs / 15712 B11.75 µs / 10920 B12.01 µs / 24664 B
map-heavy15.19 µs / 15432 B28.91 µs / 29160 B9.32 µs / 5816 B14.59 µs / 21376 B

Read it honestly: the hinted arm is ~2.4× faster than DynamicMessage with ~3× less allocation on small messages, and beats JSON both ways there; jackson wins on collection-heavy shapes, where the reflection-API cost of building repeated/map entries dominates. Wire compactness and schema are protobuf's argument regardless. The shapes are archetypes precisely because no single number describes 'protobuf vs JSON'.

Building

Bazel (with rules_clj) is the build and test harness: bazel test //.... Plain clj works too: clojure -X:test. The Clojars artifact is produced by clojure -T:build jar from the same deps.edn and version.edn.

License

Apache-2.0

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close