Lightweight field masking / projection for JSON in Go.
Supports two root modes:
- Positive (include) – default. Only explicitly included (positive) paths are emitted.
- Negative (exclude) – activated automatically when a mask expression contains only negative entries.
Uses the experimental encoding/json/v2 package for fast streaming projection without materializing intermediate maps.
Inspired by / conceptually similar to LinkedIn Rest.li Projections. kino aims to provide a lightweight, Go‑native subset focused on:
- Simple, compact expression syntax (no URI query expansion layer here).
- Runtime projection without schema code‑gen (works on any encodable value).
- Two root modes (include vs exclude) auto-detected to reduce boilerplate.
- Streaming application using
encoding/json/v2to avoid intermediate allocations.
Not (yet) implemented vs full Rest.li projections:
- Wildcards / range selectors.
- Type/schema awareness or coercion.
- Renames, aliases, or value transformations.
- Conditional operators.
These may be added selectively if they can remain ergonomic and zero/low‑overhead when unused.
go get github.com/calumari/kino
Masking a simple struct (include only selected fields + nested subfield):
package main
import (
"fmt"
"github.com/calumari/kino"
"github.com/go-json-experiment/json"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Meta struct {
Plan string `json:"plan"`
Internal string `json:"internal"`
} `json:"meta"`
}
func main() {
u := User{ID: 1, Name: "Ada", Email: "ada@example.com"}
u.Meta.Plan = "pro"
u.Meta.Internal = "secret"
// Keep id, name and meta.plan only.
mask, _ := kino.ParseMask("id,name,meta:(plan)")
out, _ := json.Marshal(u, json.WithMarshalers(kino.MarshalWithMask(mask)))
fmt.Println(string(out)) // {"id":1,"name":"Ada","meta":{"plan":"pro"}}
}
Override example (exclude whole object but keep one child):
mask, _ := kino.ParseMask("-meta:(plan)")
// Produces: {"meta":{"plan":"pro"}}
Compact, comma‑separated list of field specs:
ainclude fielda-bexclude fieldbc:(d,-e)includec.d, excludec.e- Nested:
f:(g,h)includef.g,f.h - Exclusion override:
-z:(-y,x)excludezbut keepz.x(still excludez.y)
Whitespace is ignored. Parentheses group a subtree after field:.
Root mode auto‑detection:
- If the expression has at least one positive (
a) =>Positive. - If it has only negatives (
-a,-b) =>Negative.
mask, err := kino.ParseMask("a,-b,c:(d,-e),f:(g,h),i,-z:(-y,x)")
if err != nil { /* handle error */ }
fmt.Println(mask.Mode) // Positive
fmt.Println(mask.String()) // stable string form
If you parse only negatives, e.g. -a,-b, mask.Mode == Negative.
import (
"github.com/calumari/kino"
"github.com/go-json-experiment/json"
)
projected, err := json.Marshal(value,json.WithMarshalers(kino.MarshalWithMask(mask)))
Arrays / slices inherit the same mask per element.
Masks serialize to nested objects of booleans (true = include, false = exclude). Example:
{"a":true,"b":false,"c":{"d":true,"e":false}}
Load with experimental unmarshalers (structure preserved):
var m kino.Mask
err := json.Unmarshal(data, &m, json.WithUnmarshalers(kino.MaskUnmarshalers()))
if err != nil { /* handle error */ }
In exclusion mode, a negative entry with children lets you selectively re‑include grandchildren:
Expression: -z:(x)
Result (given {"z":{"x":1, "y":2}}): {"z":{"x":1}}.