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.6"}

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
tiny69 ns / 96 B135 ns / 136 B406 ns / 448 B280 ns / 608 B884 ns / 624 B
flat495 ns / 400 B860 ns / 488 B1.61 µs / 880 B1.12 µs / 1248 B3.37 µs / 2208 B
deep1.10 µs / 744 B1.90 µs / 1712 B767 ns / 1080 B2.39 µs / 1392 B
wide-repeated4.03 µs / 3184 B4.35 µs / 2688 B2.30 µs / 1096 B7.39 µs / 4216 B
repeated-messages2.31 µs / 2360 B6.96 µs / 4560 B13.53 µs / 9816 B3.58 µs / 4024 B17.23 µs / 10424 B
map-heavy11.06 µs / 13984 B25.12 µs / 25040 B3.72 µs / 3600 B12.44 µ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 sits ~2× off protoc's own generated code and ~3× ahead of DynamicMessage on small messages, beating JSON both ways there (typed-accessor invokers via LambdaMetafactory close most of the reflection gap; see docs/design.md); jackson wins on collection-heavy shapes, where per-entry message building 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