Recently I’ve been working on a Clojure implementation for ActivityPub. In the process I wanted to use specs, but I ran into a pretty significant problem. Namely the very first line in basically every single ActivityPub JSON object: { "@context": "" }.

Do you see the problem? Well. This JSON will arrive at the server, where it’ll be handled by Cheshire or something along those lines. Point is, keys in JSON maps will end up turned into keywords. Clicked the link? The guide isn’t exactly specific about what can and can’t go into a keyword.

Still don’t see the problem? Clojure specs need to be named with auto-resolved keywords (the ones starting with double colons). Try making a keyword out of @context for me please? Yeah. You can’t just type it in naively like :@context or ::@context (it’ll throw some really nasty error at you). The reason for this, I suspect, is that the @ as used in reader macro magicks somehow clashes with the keyword definition.

There is a way around that though. Using (keyword "@context") you can define @-containing keywords, and you can namespace them too (keyword (str *ns*) "@context"). So far so good, right? Well let’s put this into a def and try using it in specs. spec/def takes namespaced keywords or symbols that can be resolved (preferably to namespaced keywords).

user=> ; first put the weird keyword into a symbol
user=> (def context (keyword (str *ns*) "@context"))
user=> ; then let's define a spec with it (included spec as s)
user=> (s/def context string?)
user=> ; now use it!
user=> (s/valid? context "hello")
Exception Unable to resolve spec: :user/@context clojure.spec.alpha/reg-resolve! (alpha.clj:69)
user=> ; wait what?
user=> context

Well. I guess I found a bug? What do I do now? I was completely lost. It seems like the library accepts the weird keyword and tries to register the spec – but then somewhere in the depths of macro hell, it fails silently and can’t look up the spec afterwards.

I almost gave up when @mpenet on Clojurians saved the day with eval. I’ll be honest: I’ve got no idea why it works. I have no idea why it was failing in the first place. But it works. Macro magick bugs undone by more macro magick.

user=> (eval `(s/def ~context string?))
user=> (s/valid? context "hello")

All’s well that ends well.