Skip to content

HomeBlog

Moving Hatamoto Go CMS to encoding/json/v2

Hatamoto's backend now runs on Go's encoding/json/v2, and a new v1 import fails the checks. Empty lists come back as [], requests are stricter, and webhook and plugin bytes are unchanged.

3 min read
𝕏in

Today, Hatamoto moves fully to the newly announced JsonV2

Hatamoto's Go backend no longer uses the original encoding/json package. We moved the HTTP layer, content storage, the platform services, the plugin host and SDK, and the tests and tooling to encoding/json/v2. Any new v1 import in the backend now fails make check, the pre-push hook and CI. The change runs on hatamoto.dev today and ships to self-hosted installs in the next release. The likely release candidate is V1.0.9

The v2 package arrived in Go 1.25 behind GOEXPERIMENT=jsonv2. We build on Go 1.27, and our release build sets no experiment flag.

What API clients will see

REST, admin and MCP responses return [] and {} for an empty list or map, where they used to return null. If your client checks for null, check for an empty list instead. The same goes for scripts that read hatamoto ... --json output, which also no longer escapes <, > and &, and neither does MCP tool output text.

Request bodies are stricter. A repeated member name, invalid UTF-8 or data after the JSON value is rejected. Member names are matched case-sensitively, which v1 never did: Title instead of title is usually ignored, and a few endpoints reject it with a 400.

What stays byte for byte the same

Webhook bodies and their HMAC signatures are unchanged, and so are the bytes the host and WASM plugins exchange. Changing them would break any receiver that checks a signature, so they go through a pinned set of options that reproduces v1's output. The one difference: a plugin type with two fields that share a JSON name now fails to marshal instead of silently dropping both.

So the JSON move doesn't break plugins built with SDK 0.5 to 0.18. A separate change in the same release stops loading older plugins that request the removed write permissions, and a package whose plugin.json repeats a field name or spells one differently (allowedHosts for allowed_hosts) is now refused on install. Check the release notes before you update. SDK 0.19.0 needs Go 1.27 or newer.

A migration normalises known mixed-case keys in stored JSON, and hatamoto doctor json reports any row that would fail a strict read. The JSON changes don't raise the rollback floor, since the previous release reads [] as well as it read null. Other changes in the same release have their own rollback steps, so read the release notes before you roll back.

How we keep v1 out

Backend code goes through a small internal/jsonx package with named option sets for requests, stored data, responses, JSON inside HTML and pinned wire formats. The contributor guide says which to use.

Three files still import v1 on purpose: two shims that read v1's legacy-compatibility options for the pinned set, and a test plugin frozen on the old encoder. Database migrations are exempt. Anywhere else in the backend, a v1 import fails the checks.

Run it on your own server.

Open source, one Go binary and Postgres. AI analytics start counting from the first request.

curl -fsSL https://hatamoto.dev/install.sh | sudo sh