Messages and validation
A message is a Go struct. Its tag names the action, and its Payload field is the body. chanx-go infers a JSON Schema from the struct, checks every incoming frame against it, and puts the same schema in the AsyncAPI document. What is documented is what is enforced.
Declaring a message
type PostPayload struct {
Body string `json:"body"` // required
Author string `json:"author" default:"anonymous"` // filled when absent
Tags []string `json:"tags,omitempty"` // optional
Reply *string `json:"reply_to,omitempty"` // optional and nullable
}
type PostMessage struct {
chanx.BaseMessage `action:"post" description:"Post something to the room."`
Payload PostPayload `json:"payload"`
}
// TypingMessage has no payload: clients send {"action": "typing"}.
type TypingMessage struct {
chanx.BaseMessage `action:"typing"`
}Tag on BaseMessage | Meaning |
|---|---|
action | the action, required: frames are routed by it |
description | the message's description, and its operation's, in AsyncAPI |
title | the AsyncAPI name; the Go type name by default |
The type name is the message's name in AsyncAPI and in generated clients, as Python uses the class name: name it PostMessage, not post.
Fields
Payload fields follow encoding/json, plus a default tag:
| Python (Pydantic) | Go |
|---|---|
body: str (required) | Body string `json:"body"` |
author: str = "anonymous" | Author string `json:"author" default:"anonymous"` |
tags: list[str] = [] | Tags []string `json:"tags,omitempty"` |
reply_to: str | None = None | Reply *string `json:"reply_to,omitempty"` |
data: Any = None | Data any `json:"data,omitempty"` |
| no payload | no Payload field: the payload is null |
A field is required unless it has omitempty, omitzero or a default. A default is filled before your handler runs:
> {"action": "post", "payload": {"body": "hello"}}
< {"action": "post", "payload": {"body": "hello", "author": "anonymous"}}What a client gets for a bad frame
The frame never reaches the handler. The client receives an error frame listing every problem, in Pydantic's format, with loc starting at the action:
> {"action": "post", "payload": {"body": 42}}
< {"action": "error", "payload": [{"type": "string_type", "loc": ["post", "payload", "body"], "msg": "Input should be a valid string"}]}An action nobody handles:
> {"action": "dance"}
< {"action": "error", "payload": [{"type": "union_tag_invalid", "loc": [], "msg": "Input tag 'dance' found using 'action' does not match any of the expected tags: 'mood', 'post', 'signup', 'typing'"}]}A text frame that is not JSON gets a json_invalid error, and a binary frame too; the connection stays open.
Strict types
Payloads are validated strictly: "3" is not an int and 1 is not a bool. Pydantic's lax mode would coerce them; chanx-go answers int_type or bool_type. Envelope fields (version, seq) stay lax on both. See Differences from Python.
Enums
A named string type with a JSONSchema method that sets Enum, Pydantic's StrEnum:
// Mood is a string enum, Pydantic's StrEnum.
type Mood string
func (Mood) JSONSchema(s *jsonschema.Schema) { s.Enum = []any{"happy", "sad"} }
type MoodPayload struct {
Mood Mood `json:"mood"`
}
type MoodMessage struct {
chanx.BaseMessage `action:"mood"`
Payload MoodPayload `json:"payload"`
}> {"action": "mood", "payload": {"mood": "angry"}}
< {"action": "error", "payload": [{"type": "enum", "loc": ["mood", "payload", "mood"], "msg": "Input should be 'happy' or 'sad'"}]}Constraints and custom checks
Two methods on the payload type cover what tags cannot say:
type SignupPayload struct {
Username string `json:"username"`
Password string `json:"password"`
}
// JSONSchema adds constraints Go tags cannot say, as Field(min_length=3) does.
// They validate frames and appear in the AsyncAPI document.
func (SignupPayload) JSONSchema(s *jsonschema.Schema) {
min := 3
s.Properties["username"].MinLength = &min
}
// Validate runs after the schema checks, as a Pydantic validator does.
func (p SignupPayload) Validate() error {
if strings.Contains(p.Password, p.Username) {
return errors.New("password must not contain the username")
}
return nil
}
type SignupMessage struct {
chanx.BaseMessage `action:"signup"`
Payload SignupPayload `json:"payload"`
}JSONSchema(*jsonschema.Schema)edits the inferred schema: lengths, ranges, patterns, formats. It is Pydantic'sField(min_length=3), and it shows in AsyncAPI. The schema type is google/jsonschema-go's.Validate() errorruns after the schema passed, with the decoded value: Pydantic's@field_validator. Its error becomes avalue_error.
> {"action": "signup", "payload": {"username": "al", "password": "x"}}
< {"action": "error", "payload": [{"type": "string_too_short", "loc": ["signup", "payload", "username"], "msg": "String should have at least 3 characters"}]}
> {"action": "signup", "payload": {"username": "ann", "password": "ann123"}}
< {"action": "error", "payload": [{"type": "value_error", "loc": ["signup", "payload"], "msg": "Value error, password must not contain the username"}]}Built-in messages
| Type | Action | Use |
|---|---|---|
chanx.PingMessage, chanx.PongMessage | ping, pong | a health check: handle ping, reply pong |
chanx.ErrorMessage | error | validation and handler errors |
chanx.AuthenticationMessage | authentication | the result of Authenticate |
chanx.CompleteMessage and others | complete... | completion frames, with SendCompletion |
chanx.SubscribeMessage and others | subscribe... | topic subscriptions |
Outside a frame
A value decoded elsewhere (an HTTP body, a queue job) does not get its defaults by itself. Call chanx.ApplyDefaults(&v) before decoding into it.
Next: Consumers.