Skip to content

Keep domain identity in a JSON round trip

An author’s ID should not be interchangeable with every string or UUID in your program. In this tutorial you define an AuthorID, encode an author as JSON, and decode it without losing either the value or its Go type. No container, server or C# knowledge is required.

This tutorial targets Fundamentals.Go v0.3.2, an experimental v0.x release. Minor releases may change APIs with migration notes. This example uses only the standard-library-only root module.

With Go 1.26 or later, run these commands in a new directory; see release status:

Terminal window
mkdir author-example
cd author-example
go mod init example.com/author-example
go get github.com/cratis/fundamentals.go@v0.3.2

Use the lowercase import path even though the GitHub repository name has capital letters. You now have a module pinned to the version this tutorial uses.

Save this complete program as main.go. It comes from the executable domain round-trip example.

package main
import (
"encoding/json"
"fmt"
"github.com/cratis/fundamentals.go/concepts"
)
type AuthorID concepts.UUID
var _ concepts.Concept[concepts.UUID] = AuthorID{}
func (id AuthorID) ConceptValue() concepts.UUID { return concepts.UUID(id) }
func (id AuthorID) MarshalText() ([]byte, error) { return concepts.UUID(id).MarshalText() }
func (id AuthorID) MarshalJSON() ([]byte, error) { return concepts.UUID(id).MarshalJSON() }
func (id *AuthorID) UnmarshalText(data []byte) error {
return (*concepts.UUID)(id).UnmarshalText(data)
}
func (id *AuthorID) UnmarshalJSON(data []byte) error {
return (*concepts.UUID)(id).UnmarshalJSON(data)
}
func main() {
id, err := concepts.ParseUUID("00112233-4455-6677-8899-AABBCCDDEEFF")
if err != nil {
panic(err)
}
type author struct {
ID AuthorID `json:"id"`
Name string `json:"name"`
}
original := author{ID: AuthorID(id), Name: "Ada"}
data, err := json.Marshal(original)
if err != nil {
panic(err)
}
var restored author
if err := json.Unmarshal(data, &restored); err != nil {
panic(err)
}
fmt.Println(string(data))
fmt.Println("same author:", restored == original)
}

AuthorID is a defined type, not an alias. It keeps the declared domain identity; ConceptValue names its exact scalar representation, concepts.UUID rather than an arbitrary [16]byte or string. The compile-time assertion checks that marker, not the correctness of your codecs.

Go defined types do not inherit methods. Without the forwarding methods, a UUID-derived value can encode as a byte array instead of a UUID string. Value receivers encode even unaddressable values, such as map entries. Pointer decoders delegate through ordinary Go pointer conversions to UUID’s failure-atomic codecs, preserving the old value on malformed input. JSON field names come from explicit tags, not a global naming policy. The example panics only to stop a failed demonstration; application code should return or handle these errors at its boundary.

Terminal window
go run .

Expected output:

{"id":"00112233-4455-6677-8899-aabbccddeeff","name":"Ada"}
same author: true

The uppercase input normalizes to lowercase dashed text. The JSON contains a scalar string, while the decoded struct still holds an AuthorID. The equality check verifies the complete round trip rather than only successful encoding. To create an ID instead of reading one, call concepts.NewUUID() and handle its error before converting the result to AuthorID.

  • Scalar reference: exact UUID/date/time/duration formats, ranges, null behavior and UUID SQL support.
  • Typed concepts: validate declarations with Underlying, check encoded bytes with CheckJSON, or recognize types in a generator.
  • Correlation context: carry an operation ID without globals.
  • Dependency injection: optional composition and ownership when plain constructors are no longer enough.

There is no automatic validation of business rules or universal serializer. A syntactically valid UUID may still be the wrong author for your operation; that check belongs to your application.