Documentation / guides
Message codecs
Understand message JSON round trips and fields that need custom codecs.
Message JSON is provider-neutral storage, not a provider wire format. Message, UserMessage, AIMessage, and SystemMessage use the embedded message codec; AIMessage and ToolResultMessage define explicit codecs so extra fields are not lost.
Wire shape
{
"role": "assistant",
"blocks": [
{"Text": "answer", "type": "text"}
],
"usage": {"InputTokens": 4, "OutputTokens": 2}
}
blocks is omitted when the slice is empty. AIMessage.Usage is omitted when nil but a present zero usage encodes as {}. ToolResultMessage always writes tool_use_id; is_error uses omitempty, so false is represented by absence and decodes back to false.
Round-trip example
package main
import (
"encoding/json"
"github.com/looprig/core/content"
)
func main() {
original := content.ToolResultMessage{
Message: content.Message{
Role: content.RoleTool,
Blocks: []content.Block{&content.TextBlock{Text: "done"}},
},
ToolUseID: "call-1",
IsError: true,
}
wire, err := json.Marshal(original)
if err != nil {
panic(err)
}
var restored content.ToolResultMessage
if err := json.Unmarshal(wire, &restored); err != nil {
panic(err)
}
}
The codec uses MarshalBlocks and UnmarshalBlocks, so nested tool-result content remains tagged. AI usage is validated on both marshal and unmarshal, and stale blocks or usage are cleared before an AIMessage decode applies new data.
Proof
- Source:
core/content/message.go,core/content/block_json.go - Tests:
core/content/message_json_test.gocovers fixed points, usage presence, nested blocks, and preservation of tool IDs.
Related: Content blocks, ToolResultMessage.