GALA’s json package provides compile-time JSON serialization powered by StructMeta[T]. No reflection, no struct tags, fully typed. All operations return Try[T] — no unchecked errors. Combined with builder pattern configuration and pattern matching, you get a clean, composable JSON pipeline.
import . "martianoff/gala/json"
struct Person(FirstName string, LastName string, Age int)
val codec = Codec[Person](SnakeCase())
val person = Person("Alice", "Smith", 30)
val jsonStr = codec.Encode(person).Get()
// => {"first_name":"Alice","last_name":"Smith","age":30}
val decoded = codec.Decode(jsonStr)
// decoded: Try[Person] — fully typed!
Create a codec with Codec[T](naming) and configure it with fluent builder methods:
val codec = Codec[Person](SnakeCase())
.Omit("Password")
.Rename("Email", "email_address")
.OmitEmpty("Bio")
Each builder method returns a new immutable codec instance — safe to share across goroutines.
| Strategy | Input | Output |
|---|---|---|
AsIs() |
FirstName |
FirstName |
CamelCase() |
FirstName |
firstName |
SnakeCase() |
FirstName |
first_name |
KebabCase() |
FirstName |
first-name |
val person = Person("Alice", "Smith", 30)
// Compact JSON
val jsonStr = codec.Encode(person).Get()
// => {"first_name":"Alice","last_name":"Smith","age":30}
// Pretty-printed JSON
val pretty = codec.EncodePretty(person).Get()
// => {
// "first_name": "Alice",
// "last_name": "Smith",
// "age": 30
// }
val decoded = codec.Decode(jsonStr)
// decoded: Try[Person]
// Safe access via Map
val name = decoded.Map((p) => p.FirstName).GetOrElse("unknown")
// Side effect on success
decoded.ForEach((p) => {
Println(s"Decoded: ${p.FirstName}, age ${p.Age}")
})
Codec instances work as pattern matching extractors via Unapply. If decoding fails, the case does not match — no exception, no panic:
val result = jsonStr match {
case codec(p) => s"Found: ${p.FirstName}, age ${p.Age}"
case _ => "invalid JSON"
}
This is especially useful when handling input from external sources:
val commandCodec = Codec[Command](SnakeCase())
val eventCodec = Codec[Event](SnakeCase())
func handleMessage(raw string) string = raw match {
case commandCodec(cmd) => processCommand(cmd)
case eventCodec(evt) => processEvent(evt)
case _ => "unknown message format"
}
Codec[T] handles arbitrarily nested struct shapes — including struct fields that are themselves Array[Struct], List[Struct], HashMap[string, Struct], or any combination of those (e.g. Array[Array[Struct]]). The user only writes Codec[Top](naming) — the transpiler discovers every reachable struct transitively, including across packages, and generates fully typed encode/decode dispatch for each.
Naming strategy propagates into nested types automatically: a SnakeCase() codec on the outermost type renames every nested struct’s fields the same way.
struct Tag(Key string, Color string)
struct User(Name string, Tags Array[Tag])
val codec = Codec[User](SnakeCase())
val tags = EmptyArray[Tag]().Append(Tag("urgent", "red")).Append(Tag("draft", "yellow"))
val user = User("alice", tags)
val jsonStr = codec.Encode(user).Get()
// => {"name":"alice","tags":[{"key":"urgent","color":"red"},{"key":"draft","color":"yellow"}]}
val decoded = codec.Decode(jsonStr).Get()
Println(s"first tag: ${decoded.Tags.Get(0).Key}/${decoded.Tags.Get(0).Color}")
The same applies to HashMap[string, Tag], List[Tag], and Array[Array[Tag]]. No additional builder calls or type annotations are required — declare the struct shape, ask for Codec[T](naming), and the codec handles the rest.
The decoder silently drops any field in the input that is not declared on the target struct. This is the default and only behaviour today — there is no strict mode or unknown-field error.
struct Point(X int, Y int)
val codec = Codec[Point](SnakeCase())
// "z" is not declared on Point — it is skipped on decode.
val raw = "{\"x\":1,\"y\":2,\"z\":99}"
val decoded = codec.Decode(raw).Get()
Println(s"x=${decoded.X} y=${decoded.Y}")
// => x=1 y=2
Skipping handles all JSON value shapes — strings, numbers, booleans, null, and nested objects/arrays — so an unknown field can carry an arbitrarily complex payload without breaking the decode.
For projects that prefer explicit package prefixes:
import "martianoff/gala/json"
val codec = json.Codec[Person](json.SnakeCase())
codec.Encode(person)
Codec[T] is powered by StructMeta[T] — a compiler intrinsic that generates type-safe field access at compile time. When you write:
val codec = Codec[Person](SnakeCase())
The transpiler:
Codec[T].Apply expects StructMeta[T] as first parameter_StructMeta_Person with typed EncodeFields / DecodeFields methodsCodec[Person]{}.Apply(_StructMeta_Person{}, SnakeCase())No reflection at runtime. Field names, types, and access patterns are all resolved at compile time.
| Method | Signature | Description |
|---|---|---|
Codec[T](naming) |
Naming → JsonEncoder[T] |
Create codec (StructMeta auto-injected) |
.Naming(n) |
Naming → JsonEncoder[T] |
Set naming strategy |
.Omit(field) |
string → JsonEncoder[T] |
Exclude field from serialization |
.Rename(field, key) |
string, string → JsonEncoder[T] |
Map field to custom JSON key |
.OmitEmpty(field) |
string → JsonEncoder[T] |
Skip field when value is zero |
.Encode(v) |
T → Try[string] |
Serialize to compact JSON |
.EncodePretty(v) |
T → Try[string] |
Serialize to pretty-printed JSON |
.Decode(s) |
string → Try[T] |
Deserialize from JSON string |
.Unapply(s) |
string → Option[T] |
Pattern matching extractor |