Liking cljdoc? Tell your friends :D

clj-protobuf

Clojars Project

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

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 B141 ns / 136 B489 ns / 448 B257 ns / 480 B935 ns / 624 B
flat545 ns / 400 B977 ns / 488 B1.94 µs / 880 B1.33 µs / 1248 B3.27 µs / 2208 B
deep744 ns / 552 B1.90 µs / 1520 B770 ns / 1080 B2.37 µs / 1392 B
wide-repeated4.12 µs / 3088 B4.08 µs / 2592 B2.64 µs / 1096 B8.04 µs / 4216 B
repeated-messages2.76 µs / 2312 B5.30 µs / 2840 B13.13 µs / 8504 B4.16 µs / 4024 B17.25 µs / 10424 B
map-heavy9.86 µs / 7512 B20.67 µs / 19000 B4.00 µs / 3616 B16.63 µs / 10720 B
enum-heavy1.21 µs / 456 B2.54 µs / 832 B1.37 µs / 1168 B4.24 µs / 2952 B

Decode (bytes → Clojure data)

shapejavahinteddynamicjsonistadata.json
tiny139 ns / 192 B209 ns / 232 B660 ns / 608 B794 ns / 1136 B643 ns / 1584 B
flat388 ns / 432 B923 ns / 600 B2.10 µs / 1096 B2.22 µs / 2168 B3.90 µs / 5776 B
deep762 ns / 1024 B2.33 µs / 2216 B1.47 µs / 2088 B1.63 µs / 3840 B
wide-repeated3.00 µs / 4296 B6.59 µs / 4664 B3.65 µs / 4104 B3.54 µs / 11496 B
repeated-messages2.47 µs / 3328 B5.72 µs / 5360 B15.49 µs / 12624 B11.55 µs / 10080 B11.10 µs / 25096 B
map-heavy13.03 µs / 13048 B30.23 µs / 29120 B11.09 µs / 5736 B18.89 µs / 22248 B
enum-heavy2.58 µs / 1056 B3.48 µs / 1440 B2.79 µs / 2840 B2.91 µs / 7200 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, and since 0.1.11 they cover repeated, map and open-enum fields too; see docs/design.md). Lists of messages now decode faster than JSON as well; jackson still wins encoding the collection-heavy shapes, where protobuf-java's own map and list 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 bazel build //src:clojars — jar and pom, from the same deps.edn and version.edn everything else here reads. bazel run //src:clojars.publish -- --dry-run prints every upload it would make.

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