replify is a Go library designed to simplify and standardize API response wrapping for RESTful services. It leverages the Decorator Pattern to dynamically add error handling, metadata, pagination, and other response features in a clean and human-readable format.
Building RESTful APIs often requires repetitive boilerplate code for standardizing responses. replify eliminates this by providing a fluent, chainable API that ensures consistent response formats across all your endpoints.
- ❌ Inconsistent response formats across different endpoints
- ❌ Repetitive error handling boilerplate in every handler
- ❌ Manual metadata management (request IDs, timestamps, versions)
- ❌ Complex pagination logic scattered throughout the codebase
- ❌ Debugging difficulties in production vs development environments
✅ Standardized response structure - One format for all endpoints
✅ Fluent API - Chainable methods for building responses
✅ Built-in pagination - Complete pagination support out of the box
✅ Metadata management - Request IDs, timestamps, API versions, locales
✅ Conditional debugging - Development-only debug information
✅ Error handling - Stack traces, error wrapping, contextual messages
✅ Type safety - Full type safety with Go generics
✅ Zero dependencies - Only uses Go standard library
- 🎯 Standardized JSON Format - Consistent structure across all API responses
- 🔗 Fluent Builder Pattern - Chain methods to construct complex responses
- 📄 Pagination Support - Built-in page, per_page, total_items, total_pages, is_last
- 🔍 Request Tracing - Track requests with unique IDs across microservices
- 🌍 Internationalization - Locale support for multi-language APIs
- 🐛 Debug Mode - Conditional debugging information for development
- ⚡ Error Handling - Rich error information with stack traces
- 📊 Metadata - API version, custom fields, timestamps
- ✅ Status Helpers - IsSuccess(), IsClientError(), IsServerError()
- 🔄 JSON Parsing - Parse JSON strings back to wrapper objects
- Go version 1.23 or higher
# Latest version
go get -u github.com/sivaosorg/replify@latest
# Specific version
go get github.com/sivaosorg/replify@v0.0.1import "github.com/sivaosorg/replify"With Go's module support, go [build|run|test] automatically fetches the necessary dependencies when you add the import.
package main
import (
"fmt"
"github.com/sivaosorg/replify"
)
func main() {
// Create a simple success response
response := replify.New().
WithStatusCode(200).
WithMessage("User retrieved successfully").
WithBody(map[string]string{
"id": "123",
"name": "John Doe",
})
fmt.Println(response.JSONPretty())
}Output:
{
"data": {
"id": "123",
"name": "John Doe"
},
"headers": {
"code": 200,
"text": "OK"
},
"message": "User retrieved successfully",
"meta": {
"api_version": "v0.0.1",
"locale": "en_US",
"request_id": "d7e5ce24b796da94770911db36565bf9",
"requested_time": "2026-01-29T10:07:05.751501+07:00"
},
"status_code": 200,
"total": 0
}The library produces responses in this standardized format:
{
"status_code": 200,
"message": "Resource retrieved successfully",
"path": "/api/v1/users",
"data": [ // abstract data (can be array or object)
{
"id": "user_01J6G7W9K2M4X7V5P8B3Q2Z1NS",
"username": "jdoe_dev",
"email": "j.doe@example.com",
"role": "administrator",
"status": "active",
"created_at": "2025-01-15T08:30:00Z",
"last_login": "2026-02-26T14:15:22Z"
},
{
"id": "user_01J6G7W9K2M4X7V5P8B3Q2Z1NT",
"username": "s_smith",
"email": "sarah.smith@example.com",
"role": "editor",
"status": "active",
"created_at": "2025-02-01T10:15:00Z",
"last_login": "2026-02-25T09:45:10Z"
}
],
"pagination": {
"page": 1,
"per_page": 2,
"total_items": 120,
"total_pages": 60,
"is_last": false
},
"meta": {
"request_id": "req_80eafc6a1655ec5a06595d155f1e6951",
"api_version": "v1.0.4",
"locale": "en_US",
"requested_time": "2026-02-26T17:30:28.983Z",
"custom_fields": { // custom fields
"trace_id": "80eafc6a1655ec5a06595d155f1e6951",
"origin_region": "us-east-1"
}
},
"debug": { // custom fields
"trace_session_id": "4919e84fc26881e9fe790f5d07465db4",
"execution_time_ms": 42
}
}| Field | Type | Description |
|---|---|---|
data |
interface{} |
The primary data payload of the response |
status_code |
int |
HTTP status code for the response |
message |
string |
Human-readable message providing context |
total |
int |
Total number of items (used in non-paginated responses) |
path |
string |
Request path for which the response is generated |
meta |
object |
Metadata about the API response |
meta.request_id |
string |
Unique identifier for the request, useful for debugging |
meta.api_version |
string |
API version used for the request |
meta.locale |
string |
Locale used for the request (e.g., "en_US") |
meta.requested_time |
string |
Timestamp when the request was made (ISO 8601) |
meta.custom_fields |
object |
Additional custom metadata fields |
pagination |
object |
Pagination details, if applicable |
pagination.page |
int |
Current page number |
pagination.per_page |
int |
Number of items per page |
pagination.total_items |
int |
Total number of items available |
pagination.total_pages |
int |
Total number of pages |
pagination.is_last |
bool |
Indicates whether this is the last page |
debug |
object |
Debugging information (useful for development) |
response := replify.New().
WithStatusCode(200).
WithMessage("Operation successful").
WithBody(data)response := replify.New().
WithStatusCode(400).
WithError("Invalid input: email is required").
WithMessage("Validation failed")response := replify.New().
WithStatusCode(200).
WithBody(users).
WithRequestID("req-123-456").
WithApiVersion("v1.0.0").
WithLocale("en_US").
WithPath("/api/v1/users")pagination := replify.Pages().
WithPage(1).
WithPerPage(20).
WithTotalItems(150).
WithTotalPages(8).
WithIsLast(false)
response := replify.New().
WithStatusCode(200).
WithBody(users).
WithPagination(pagination).
WithTotal(20)response := replify.New().
WithStatusCode(500).
WithError("Database connection failed").
WithDebuggingKV("query", "SELECT * FROM users").
WithDebuggingKV("error_code", "CONN_TIMEOUT").
WithDebuggingKV("retry_count", 3)package main
import (
"fmt"
"github.com/sivaosorg/replify"
"github.com/sivaosorg/replify/pkg/randn"
)
func main() {
// Create pagination
p := replify.Pages().
WithIsLast(true).
WithPage(1000).
WithTotalItems(120).
WithTotalPages(34).
WithPerPage(2)
// Create response
w := replify.New().
WithStatusCode(200).
WithTotal(1).
WithMessagef("How are you? %v", "I'm good").
WithDebuggingKV("refer", 1234).
WithDebuggingKVf("___abc", "trace sessions_id: %v", randn.CryptoID()).
WithBody("response body here").
WithPath("/api/v1/users").
WithCustomFieldKVf("fields", "userID: %v", 103).
WithPagination(p)
if !w.Available() {
return
}
// Access response properties
fmt.Println(w.JSON())
fmt.Println(w.StatusCode())
fmt.Println(w.StatusText())
fmt.Println(w.Message())
fmt.Println(w.Body())
fmt.Println(w.IsSuccess())
fmt.Println(w.Respond())
// Check metadata
fmt.Println(w.Meta().IsCustomPresent())
fmt.Println(w.Meta().IsApiVersionPresent())
fmt.Println(w.Meta().IsRequestIDPresent())
fmt.Println(w.Meta().IsRequestedTimePresent())
}package main
import (
"fmt"
"log"
"time"
"github.com/sivaosorg/replify"
)
func main() {
jsonStr := `{
"data": "response body here",
"debug": {
"___abc": "trace sessions_id: 4919e84fc26881e9fe790f5d07465db4",
"refer": 1234
},
"message": "How do you do? I'm good",
"meta": {
"api_version": "v0.0.1",
"custom_fields": {
"fields": "userID: 103"
},
"locale": "en_US",
"request_id": "80eafc6a1655ec5a06595d155f1e6951",
"requested_time": "2024-12-14T20:24:23.983839+07:00"
},
"pagination": {
"is_last": true,
"page": 1000,
"per_page": 2,
"total_items": 120,
"total_pages": 34
},
"path": "/api/v1/users",
"status_code": 200,
"total": 1
}`
t := time.Now()
w, err := replify.UnwrapJSON(jsonStr)
diff := time.Since(t)
if err != nil {
log.Fatalf("Error parsing JSON: %v", err)
}
fmt.Printf("Exe time: %+v\n", diff.String())
fmt.Printf("%+v\n", w.OnDebugging("___abc"))
fmt.Printf("%+v\n", w.JSONPretty())
}package main
import (
"encoding/json"
"net/http"
"github.com/sivaosorg/replify"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// GET /users/:id
func GetUser(w http.ResponseWriter, r *http.Request) {
id := getIDFromPath(r)
user, err := findUserByID(id)
var response *replify.R
if err != nil {
response = replify.New().
WithStatusCode(404).
WithError(err.Error()).
WithMessage("User not found").
WithRequestID(r.Header.Get("X-Request-ID"))
} else {
response = replify.New().
WithStatusCode(200).
WithBody(user).
WithMessage("User retrieved successfully").
WithRequestID(r.Header.Get("X-Request-ID"))
}
respondJSON(w, response)
}
// POST /users
func CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
response := replify.New().
WithStatusCode(400).
WithError(err.Error()).
WithMessage("Invalid request body")
respondJSON(w, response)
return
}
if err := validateUser(user); err != nil {
response := replify.New().
WithStatusCode(422).
WithError(err.Error()).
WithMessage("Validation failed")
respondJSON(w, response)
return
}
createdUser, err := createUser(user)
if err != nil {
response := replify.New().
WithStatusCode(500).
WithErrorAck(err).
WithMessage("Failed to create user")
respondJSON(w, response)
return
}
response := replify.New().
WithStatusCode(201).
WithBody(createdUser).
WithMessage("User created successfully")
respondJSON(w, response)
}
func respondJSON(w http.ResponseWriter, response *replify.R) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode())
w.Write([]byte(response.JSON()))
}func ListUsers(w http.ResponseWriter, r *http.Request) {
// Parse query parameters
page := getQueryInt(r, "page", 1)
perPage := getQueryInt(r, "per_page", 10)
search := r.URL.Query().Get("search")
// Fetch users with pagination
users, total, err := db.FindUsers(search, page, perPage)
if err != nil {
response := replify.New().
WithStatusCode(500).
WithErrorAck(err).
WithMessage("Failed to fetch users").
WithDebuggingKV("search", search).
WithDebuggingKV("page", page)
respondJSON(w, response)
return
}
// Calculate pagination metadata
totalPages := (total + perPage - 1) / perPage
isLast := page >= totalPages
pagination := replify.Pages().
WithPage(page).
WithPerPage(perPage).
WithTotalItems(total).
WithTotalPages(totalPages).
WithIsLast(isLast)
response := replify.New().
WithStatusCode(200).
WithBody(users).
WithPagination(pagination).
WithTotal(len(users)).
WithMessage("Users retrieved successfully").
WithPath(r.URL.Path).
WithRequestID(r.Header.Get("X-Request-ID"))
respondJSON(w, response)
}func ProcessOrder(w http.ResponseWriter, r *http.Request) {
order, err := processOrderLogic(r)
response := replify.New()
if err != nil {
response.
WithStatusCode(500).
WithErrorAck(err).
WithMessage("Order processing failed")
// Add debug info in development
if os.Getenv("ENV") == "development" {
response.
WithDebuggingKV("timestamp", time.Now()).
WithDebuggingKV("stack_trace", err.Error()).
WithDebuggingKV("order_data", order)
}
} else {
response.
WithStatusCode(200).
WithBody(order).
WithMessage("Order processed successfully")
}
respondJSON(w, response)
}type R struct {
*wrapper
}The R type is a high-level abstraction providing a simplified interface for handling API responses.
| Function | Description |
|---|---|
New() *wrapper |
Creates a new response wrapper |
Pages() *pagination |
Creates a new pagination object |
UnwrapJSON(jsonStr string) (*wrapper, error) |
Parses JSON string to wrapper |
| Method | Description |
|---|---|
WithStatusCode(code int) |
Sets HTTP status code |
WithBody(v interface{}) |
Sets response body/data |
WithMessage(message string) |
Sets response message |
WithMessagef(format string, args...) |
Sets formatted message |
WithError(message string) |
Sets error message |
WithErrorf(format string, args...) |
Sets formatted error |
WithErrorAck(err error) |
Sets error with stack trace |
AppendError(err error, message string) |
Wraps error with context |
AppendErrorf(err error, format string, args...) |
Wraps error with formatted context |
WithPath(v string) |
Sets request path |
WithPathf(v string, args...) |
Sets formatted request path |
WithTotal(total int) |
Sets total items count |
| Method | Description |
|---|---|
WithRequestID(v string) |
Sets request ID |
WithRequestIDf(format string, args...) |
Sets formatted request ID |
WithApiVersion(v string) |
Sets API version |
WithApiVersionf(format string, args...) |
Sets formatted API version |
WithLocale(v string) |
Sets locale (e.g., "en_US") |
WithRequestedTime(v time.Time) |
Sets request timestamp |
WithCustomFieldKV(key string, value interface{}) |
Adds custom metadata field |
WithCustomFieldKVf(key, format string, args...) |
Adds formatted custom field |
WithCustomFields(values map[string]interface{}) |
Sets multiple custom fields |
WithMeta(v *meta) |
Sets entire metadata object |
WithHeader(v *header) |
Sets the header |
| Method | Description |
|---|---|
WithPagination(v *pagination) |
Sets pagination object |
WithPage(v int) |
Sets current page number |
WithPerPage(v int) |
Sets items per page |
WithTotalItems(v int) |
Sets total items count |
WithTotalPages(v int) |
Sets total pages count |
WithIsLast(v bool) |
Sets if current page is last |
| Method | Description |
|---|---|
WithDebugging(v map[string]interface{}) |
Sets debug information map |
WithDebuggingKV(key string, value interface{}) |
Adds single debug key-value |
WithDebuggingKVf(key, format string, args...) |
Adds formatted debug value |
| Method | Returns | Description |
|---|---|---|
Available() |
bool |
Checks if wrapper is non-nil |
StatusCode() |
int |
Gets HTTP status code |
StatusText() |
string |
Gets status text (e.g., "OK") |
Body() |
interface{} |
Gets response body |
Message() |
string |
Gets response message |
Error() |
string |
Gets error message |
Cause() |
error |
Gets underlying error cause |
Total() |
int |
Gets total items |
Meta() |
*meta |
Gets metadata object |
Header() |
*header |
Gets header object |
Pagination() |
*pagination |
Gets pagination object |
Debugging() |
map[string]interface{} |
Gets debug information |
OnDebugging(key string) |
interface{} |
Gets specific debug value |
| Method | Returns | Description |
|---|---|---|
IsSuccess() |
bool |
Checks if status is 2xx |
IsClientError() |
bool |
Checks if status is 4xx |
IsServerError() |
bool |
Checks if status is 5xx |
IsRedirection() |
bool |
Checks if status is 3xx |
IsError() |
bool |
Checks if error exists or status is 4xx/5xx |
IsErrorPresent() |
bool |
Checks if error field exists |
IsBodyPresent() |
bool |
Checks if body exists |
IsPagingPresent() |
bool |
Checks if pagination exists |
IsMetaPresent() |
bool |
Checks if metadata exists |
IsHeaderPresent() |
bool |
Checks if header exists |
IsDebuggingPresent() |
bool |
Checks if debug info exists |
IsDebuggingKeyPresent(key string) |
bool |
Checks if specific debug key exists |
IsLastPage() |
bool |
Checks if current page is last |
IsStatusCodePresent() |
bool |
Checks if valid status code exists |
IsTotalPresent() |
bool |
Checks if total count exists |
| Method | Returns | Description |
|---|---|---|
JSON() |
string |
Returns compact JSON string |
JSONPretty() |
string |
Returns pretty-printed JSON |
Respond() |
map[string]interface{} |
Returns map representation |
Reply() |
R |
Returns R wrapper |
| Scenario | HTTP Status Codes | Example |
|---|---|---|
| Successful Resource Retrieval | 200 OK, 304 Not Modified | GET /users/123 - Returns user data |
| Resource Creation | 201 Created | POST /users - Creates a new user |
| Asynchronous Processing | 202 Accepted | POST /large-file - File upload starts |
| Validation Errors | 400 Bad Request | POST /users - Missing required field |
| Authentication Issues | 401 Unauthorized, 403 Forbidden | Invalid credentials or permissions |
| Rate Limiting | 429 Too Many Requests | Exceeded API request limits |
| Missing Resource | 404 Not Found | GET /users/999 - User not found |
| Server Failures | 500 Internal Server Error, 503 Service Unavailable | Database failure or maintenance |
| Version Conflicts | 409 Conflict | Outdated version causing conflict |
| Code | Status | Use Case |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (resource created) |
| 202 | Accepted | Async processing started |
| 204 | No Content | Successful DELETE |
| 206 | Partial Content | Video streaming, range requests |
| Code | Status | Use Case |
|---|---|---|
| 301 | Moved Permanently | Resource permanently moved |
| 302 | Found | Temporary redirect |
| 304 | Not Modified | Cached content still valid |
| 307 | Temporary Redirect | POST redirect maintaining method |
| 308 | Permanent Redirect | Permanent redirect maintaining method |
| Code | Status | Use Case |
|---|---|---|
| 400 | Bad Request | Invalid request format/data |
| 401 | Unauthorized | Missing/invalid authentication |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource conflict (duplicate) |
| 413 | Payload Too Large | Request body too large |
| 415 | Unsupported Media Type | Invalid content type |
| 422 | Unprocessable Entity | Validation errors |
| 429 | Too Many Requests | Rate limiting |
| Code | Status | Use Case |
|---|---|---|
| 500 | Internal Server Error | Unexpected server error |
| 501 | Not Implemented | Feature not implemented |
| 502 | Bad Gateway | Upstream service error |
| 503 | Service Unavailable | Service down/maintenance |
| 504 | Gateway Timeout | Upstream timeout |
-
Always set status codes
response := replify.New(). WithStatusCode(200). WithBody(data)
-
Use request IDs for tracing
response := replify.New(). WithRequestID(r.Header.Get("X-Request-ID")). WithBody(data)
-
Include API version
response := replify.New(). WithApiVersion("v1.0.0"). WithBody(data)
-
Use WithErrorAck for stack traces
response := replify.New(). WithStatusCode(500). WithErrorAck(err)
-
Check response status before processing
if response.IsSuccess() { processData(response.Body()) }
-
Use pagination for list endpoints
pagination := replify.Pages(). WithPage(page). WithPerPage(perPage). WithTotalItems(total)
-
Don't forget to set status codes
// ❌ Bad response := replify.New().WithBody(data) // ✅ Good response := replify.New().WithStatusCode(200).WithBody(data)
-
Don't expose sensitive debug info in production
// ❌ Bad response := replify.New(). WithDebuggingKV("database_password", dbPass) // ✅ Good if os.Getenv("ENV") == "development" { response.WithDebuggingKV("query", sqlQuery) }
-
Don't use generic error messages
// ❌ Bad WithError("Error occurred") // ✅ Good WithError("Failed to create user: email already exists")
-
Don't ignore error checking
// ❌ Bad wrapper, _ := replify.UnwrapJSON(jsonStr) // ✅ Good wrapper, err := replify.UnwrapJSON(jsonStr) if err != nil { log.Printf("Failed to parse JSON: %v", err) }
- RESTful API Development - Standardizing API responses
- Microservices - Consistent responses across services
- API Versioning - Including version metadata
- Error Standardization - Consistent error formats
- Pagination - APIs returning paginated results
- Multi-tenant APIs - Including tenant/locale information
- Request Tracing - Tracking requests across services
- Development Debugging - Conditional debug information
- GraphQL APIs - GraphQL has its own response format
- gRPC Services - Protocol Buffers define the structure
- WebSocket APIs - Real-time bidirectional communication
- Simple CLIs - Overkill for command-line tools
- Internal Services - Where custom formats are required
- High-Performance - Direct JSON encoding may be faster
fj (Fast JSON) is the JSON path-extraction engine embedded in replify. It lets you read, query, and transform values from a JSON document without unmarshalling the entire structure into Go types. It lives in pkg/fj and is exposed through the wrapper type in parser.go.
When a wrapper carries a JSON body, fj powers every field-level query on that body. Instead of decoding the whole payload into a map[string]any or a concrete struct, fj walks the raw string just far enough to locate the requested path. This keeps allocations low and throughput high on hot request paths.
HTTP Request → wrapper.WithBody(data) → wrapper.QueryJSONBody("user.name")
↓
fj.Get(jsonString, "user.name")
↓
fj.Context ← single value, no full decode
| Scenario | Recommended approach |
|---|---|
| Extract one or a few fields from a large response body | fj / QueryJSONBody |
| Validate that the body is well-formed JSON | fj.IsValidJSON / ValidJSONBody |
| Search leaf values or keys across an unknown schema | fj.Search / SearchJSONBody* |
| Apply streaming transforms (pretty-print, minify, etc.) | fj transformers |
| Bind the full payload into a typed struct | encoding/json or json-iterator |
| Write or modify JSON | encoding/json |
| JSON schema validation | a dedicated schema library |
user.name field access
roles.0 array index
roles.# array length
roles.#.name collect field from every element
roles.#(role=="admin") first element where role == "admin"
roles.#(role=="admin")# all elements where role == "admin"
{id,name} multi-selector → new object
[id,name] multi-selector → new array
name.@uppercase built-in transformer
name.@word:upper transformer with argument
..title recursive descent (JSON Lines / deep scan)
Dots and wildcards in key names can be escaped with a backslash (\).
import "github.com/sivaosorg/replify/pkg/fj"
json := `{
"user": {"name": "Alice", "age": 30, "active": true},
"roles": ["admin", "editor"],
"scores": [95, 87, 92]
}`
// Single path
name := fj.Get(json, "user.name").String() // "Alice"
age := fj.Get(json, "user.age").Int64() // 30
ok := fj.Get(json, "user.active").Bool() // true
n := fj.Get(json, "roles.#").Int() // 2 (array length)
// Multiple paths in one pass
results := fj.GetMulti(json, "user.name", "user.age", "roles.#")
// results[0].String() == "Alice", results[1].Int64() == 30, results[2].Int() == 2
// Check presence before use
if ctx := fj.Get(json, "user.email"); ctx.Exists() {
fmt.Println(ctx.String())
}
// Parse a document once, query multiple times (avoids re-parsing)
doc := fj.Parse(json)
fmt.Println(doc.Get("user.name").String())
fmt.Println(doc.Get("roles.0").String())GetBytes is preferred when you already hold a []byte. It uses unsafe pointer operations internally to avoid an extra string allocation:
rawBytes := []byte(`{"id":42,"status":"active"}`)
id := fj.GetBytes(rawBytes, "id").Int() // 42
status := fj.GetBytes(rawBytes, "status").String() // "active"
// Multiple paths from bytes
res := fj.GetBytesMulti(rawBytes, "id", "status")Memory note:
fj.Context.Raw()returns a substring view of the original string without copying. Do not hold a reference to theContextafter the source string has been released; the backing memory will be reclaimed.
The wrapper type exposes all fj operations without requiring you to import pkg/fj directly in most cases:
response := replify.New().
WithStatusCode(200).
WithBody(map[string]any{
"user": map[string]any{"name": "Alice", "role": "admin"},
"items": []map[string]any{
{"id": 1, "price": 9.99},
{"id": 2, "price": 4.50},
},
})
// Single path query
name := response.QueryJSONBody("user.name").String() // "Alice"
// Multiple paths in one call (one JSON serialization)
fields := response.QueryJSONBodyMulti("user.name", "user.role")
fmt.Println(fields[0].String(), fields[1].String()) // Alice admin
// Parse the body once and chain subsequent queries
ctx := response.JSONBodyParser()
fmt.Println(ctx.Get("user.name").String())
fmt.Println(ctx.Get("items.#").Int()) // array length
// Validate the body
if !response.ValidJSONBody() {
log.Println("body is not valid JSON")
return
}
// Aggregate helpers
total := response.SumJSONBody("items.#.price") // 14.49
min, _ := response.MinJSONBody("items.#.price") // 4.50
max, _ := response.MaxJSONBody("items.#.price") // 9.99
avg, _ := response.AvgJSONBody("items.#.price") // 7.245
fmt.Println(name)
fmt.Println(fields[0].String(), fields[1].String())
fmt.Println(ctx.Get("user.name").String())
fmt.Println(ctx.Get("items.#").Int())
fmt.Println(total)
fmt.Println(min)
fmt.Println(max)
fmt.Println(avg)Performance tip:
QueryJSONBodyserializes the body on every call. For repeated queries on the same body, callJSONBodyParser()once and reuse the returnedfj.Context.
A fj.Context is returned by every query. Always call .Exists() before using the value if the path might be absent.
ctx := fj.Get(json, "optional.field")
ctx.Exists() // false when path is missing
ctx.Kind() // fj.Null | fj.String | fj.Number | fj.True | fj.False | fj.JSON
ctx.String() // string representation
ctx.Bool() // bool
ctx.Int() // int
ctx.Int64() // int64
ctx.Float64() // float64
ctx.Raw() // raw JSON token (no allocation)
ctx.IsArray() // true when kind == JSON and raw starts with '['
ctx.IsObject() // true when kind == JSON and raw starts with '{'
ctx.IsError() // true if parsing produced an error
ctx.Cause() // error string, or "" if no error
// Iterate array values
ctx.Foreach(func(key, val fj.Context) bool {
fmt.Println(val.String())
return true // return false to stop
})Transformers are applied with the @ prefix inside a path expression and receive the current JSON value as input. An optional argument is passed after a : separator.
path.@transformerName
path.@transformerName:argument
path.@transformerName:{"key":"value"}
| Transformer | Alias(es) | Input | Description |
|---|---|---|---|
@pretty |
— | any | Pretty-print (indented) JSON. Accepts optional {"sort_keys":true,"indent":"\t","prefix":"","width":80}. |
@minify |
@ugly |
any | Compact single-line JSON (all whitespace removed). |
@valid |
— | any | Returns "true" / "false" — whether the input is valid JSON. |
@this |
— | any | Identity — returns the input unchanged. |
@reverse |
— | array | object | Reverses element order (array) or key order (object). |
@flatten |
— | array | Shallow-flatten nested arrays. Pass {"deep":true} to recurse. |
@join |
— | array of objects | Merge an array of objects into one object. Pass {"preserve":true} to keep duplicate keys. |
@keys |
— | object | Return a JSON array of the object's keys. |
@values |
— | object | Return a JSON array of the object's values. |
@group |
— | object of arrays | Zip object-of-arrays into an array-of-objects. |
@search |
— | any | @search:path — collect all values reachable at path anywhere in the tree. |
@json |
— | string | Parse the string as JSON and return the value. |
@string |
— | any | Encode the value as a JSON string literal. |
| Transformer | Alias(es) | Description |
|---|---|---|
@uppercase |
@upper |
Convert all characters to upper-case. |
@lowercase |
@lower |
Convert all characters to lower-case. |
@flip |
— | Reverse the characters of the string. |
@trim |
— | Strip leading/trailing whitespace. |
@snakecase |
@snake, @snakeCase |
Convert to snake_case. |
@camelcase |
@camel, @camelCase |
Convert to camelCase. |
@kebabcase |
@kebab, @kebabCase |
Convert to kebab-case. |
@replace |
— | @replace:{"target":"old","replacement":"new"} — replace first occurrence. |
@replaceAll |
— | @replaceAll:{"target":"old","replacement":"new"} — replace all occurrences. |
@hex |
— | Hex-encode the value. |
@bin |
— | Binary-encode the value. |
@insertAt |
— | @insertAt:{"index":5,"insert":"XYZ"} — insert a substring at position. |
@wc |
— | Return the word-count of a string as an integer. |
@padLeft |
— | @padLeft:{"padding":"*","length":10} — left-pad to a fixed width. |
@padRight |
— | @padRight:{"padding":"*","length":10} — right-pad to a fixed width. |
| Transformer | Description |
|---|---|
@project |
Pick and/or rename fields from an object. Arg: {"pick":["f1","f2"],"rename":{"f1":"newName"}}. Omit pick to keep all fields; omit rename for no renaming. |
@default |
Inject fallback values for fields that are absent or null. Arg: {"field":"defaultValue",...}. Existing non-null fields are never overwritten. |
| Transformer | Description |
|---|---|
@filter |
Keep only elements matching a condition. Arg: {"key":"field","op":"eq","value":val}. Operators: eq (default), ne, gt, gte, lt, lte, contains. |
@pluck |
Extract a named field (supports dot-notation paths) from every element. Arg: field path string, e.g. @pluck:name or @pluck:addr.city. |
@first |
Return the first element of the array, or null if empty. |
@last |
Return the last element of the array, or null if empty. |
@count |
Return the number of elements (array) or key-value pairs (object) as an integer. Scalars return 0. |
@sum |
Sum all numeric values in the array; non-numeric elements are skipped. Returns 0 for empty arrays. |
@min |
Return the minimum numeric value in the array. Returns null when no numbers are present. |
@max |
Return the maximum numeric value in the array. Returns null when no numbers are present. |
| Transformer | Description |
|---|---|
@coerce |
Convert a scalar to a target type. Arg: {"to":"string"}, {"to":"number"}, or {"to":"bool"}. Objects and arrays are returned unchanged. |
json := `{
"user": {"name": "Alice", "role": null, "age": 30, "city": "NY"},
"scores": [95, 87, 92, 78],
"users": [
{"name": "Alice", "active": true, "addr": {"city": "NY"}},
{"name": "Bob", "active": false, "addr": {"city": "LA"}},
{"name": "Carol", "active": true, "addr": {"city": "NY"}}
]
}`
// ── Core ─────────────────────────────────────────────────────────────────────
fj.Get(json, "@pretty").String() // indented JSON
fj.Get(json, "@minify").String() // compact JSON
fj.Get(json, "user.@keys").String() // ["name","role","age","city"]
fj.Get(json, "user.@values").String() // ["Alice",null,30,"NY"]
fj.Get(json, "user.@valid").String() // "true"
// ── String ───────────────────────────────────────────────────────────────────
fj.Get(json, "user.name.@uppercase").String() // "ALICE"
fj.Get(json, "user.name.@reverse").String() // "ecilA"
fj.Get(json, "user.name.@snakecase").String() // "alice"
fj.Get(json, "user.city.@padLeft:{\"padding\":\"0\",\"length\":6}").String() // "000 NY"
// ── Object ───────────────────────────────────────────────────────────────────
// Project: keep only name and age, rename age → years
fj.Get(json, `user.@project:{"pick":["name","age"],"rename":{"age":"years"}}`).Raw()
// → {"name":"Alice","years":30}
// Default: fill in missing / null fields
fj.Get(json, `user.@default:{"role":"viewer","active":true}`).Raw()
// → {"name":"Alice","role":"viewer","age":30,"city":"NY","active":true}
// ── Array ────────────────────────────────────────────────────────────────────
// Filter: keep only active users
fj.Get(json, `users.@filter:{"key":"active","value":true}`).Raw()
// → [{"name":"Alice","active":true,...},{"name":"Carol","active":true,...}]
// Pluck: extract the city from every user's address
fj.Get(json, `users.@pluck:addr.city`).Raw()
// → ["NY","LA","NY"]
// Aggregation helpers
fj.Get(json, "scores.@first").Raw() // 95
fj.Get(json, "scores.@last").Raw() // 78
fj.Get(json, "scores.@count").Raw() // 4
fj.Get(json, "scores.@sum").Raw() // 352
fj.Get(json, "scores.@min").Raw() // 78
fj.Get(json, "scores.@max").Raw() // 95
// ── Coerce ───────────────────────────────────────────────────────────────────
fj.Get(`42`, `@coerce:{"to":"string"}`).Raw() // "42"
fj.Get(`"99"`, `@coerce:{"to":"number"}`).Raw() // 99
fj.Get(`1`, `@coerce:{"to":"bool"}`).Raw() // trueTransformers can be chained using the | pipe operator or dot notation:
// First filter the array, then count the remaining elements
fj.Get(json, `users.@filter:{"key":"active","value":true}|@count`).Raw()
// → 2
// Pluck names, then reverse the resulting array
fj.Get(json, `users.@pluck:name|@reverse`).Raw()
// → ["Carol","Bob","Alice"]The following scenarios demonstrate how to combine multiple transformers into a single expression to process realistic JSON payloads.
Example 1 — E-commerce product catalog: filter, aggregate, and shape
catalog := `{
"products": [
{"id":"p1","name":"Laptop Pro", "category":"electronics","price":1299.99,"stock":5},
{"id":"p2","name":"USB-C Hub", "category":"electronics","price":49.99, "stock":120},
{"id":"p3","name":"Desk Chair", "category":"furniture", "price":349.00, "stock":0},
{"id":"p4","name":"Standing Desk", "category":"furniture", "price":699.00, "stock":3},
{"id":"p5","name":"Webcam HD", "category":"electronics","price":89.99, "stock":45}
]
}`
// All in-stock electronics names
fj.Get(catalog, `products.@filter:{"key":"category","value":"electronics"}|@filter:{"key":"stock","op":"gt","value":0}|@pluck:name`).Raw()
// → ["Laptop Pro","USB-C Hub","Webcam HD"]
// Count of in-stock products
fj.Get(catalog, `products.@filter:{"key":"stock","op":"gt","value":0}|@count`).Raw()
// → 4
// Price range of in-stock products
fj.Get(catalog, `products.@filter:{"key":"stock","op":"gt","value":0}|@pluck:price|@min`).Raw()
// → 49.99
fj.Get(catalog, `products.@filter:{"key":"stock","op":"gt","value":0}|@pluck:price|@max`).Raw()
// → 1299.99
// Project the first in-stock product as a display card (pick and rename fields)
first := fj.Get(catalog, `products.@filter:{"key":"stock","op":"gt","value":0}|@first`).Raw()
fj.Get(first, `@project:{"pick":["name","price"],"rename":{"name":"title","price":"cost"}}`).Raw()
// → {"title":"Laptop Pro","cost":1299.99}Example 2 — API response normalization: fill defaults then project and rename
// Raw user record from an external API with null / absent fields
rawUser := `{"id":"u1","name":"Alice","role":null,"verified":null}`
// One-shot normalization: fill nulls → keep only safe fields → rename id for the frontend
fj.Get(rawUser, `@default:{"role":"viewer","verified":false}|@project:{"pick":["id","name","role","verified"],"rename":{"id":"userId"}}`).Raw()
// → {"userId":"u1","name":"Alice","role":"viewer","verified":false}Example 3 — Log processing: filter, count, and retrieve the latest entry
logs := `[
{"level":"error","msg":"Connection refused","ts":1700001},
{"level":"info", "msg":"Server started", "ts":1700002},
{"level":"error","msg":"Timeout exceeded", "ts":1700003},
{"level":"warn", "msg":"High memory", "ts":1700004}
]`
// How many errors?
fj.Get(logs, `@filter:{"key":"level","value":"error"}|@count`).Raw()
// → 2
// All error messages
fj.Get(logs, `@filter:{"key":"level","value":"error"}|@pluck:msg`).Raw()
// → ["Connection refused","Timeout exceeded"]
// Most recent error entry (last in the filtered array)
fj.Get(logs, `@filter:{"key":"level","value":"error"}|@last`).Raw()
// → {"level":"error","msg":"Timeout exceeded","ts":1700003}Example 4 — Nested data aggregation: filter → pluck → flatten → sum
teamData := `{
"teams": [
{"name":"Alpha","active":true, "monthly_revenue":[10000,12000,11000]},
{"name":"Beta", "active":false,"monthly_revenue":[8000,9000,8500]},
{"name":"Gamma","active":true, "monthly_revenue":[15000,16000,14000]}
]
}`
// Total revenue across all active teams, flattening the per-team monthly arrays first
fj.Get(teamData, `teams.@filter:{"key":"active","value":true}|@pluck:monthly_revenue|@flatten|@sum`).Raw()
// → 78000 (Alpha: 33000 + Gamma: 45000)Example 5 — URL-slug generation from a display name
// Multi-word title with duplicate internal spaces → URL-safe kebab-case slug
fj.Get(`"My Blog Post Title"`, `@trim|@lowercase|@kebabcase`).Raw()
// → "my-blog-post-title"
// Author name to lowercase slug
fj.Get(`"John Doe"`, `@lowercase|@replace:{"target":" ","replacement":"-"}`).Raw()
// → "john-doe"Example 6 — Config merging and introspection
// Merge two partial config objects; later values overwrite earlier ones for duplicate keys
overrides := `[{"host":"localhost","port":5432},{"port":5433,"ssl":true}]`
merged := fj.Get(overrides, `@join`).Raw()
// → {"host":"localhost","port":5433,"ssl":true}
// Inspect which keys are present after the merge
fj.Get(merged, `@keys`).Raw()
// → ["host","port","ssl"]
// Count the merged keys
fj.Get(merged, `@count`).Raw()
// → 3
// Project only the connection-relevant subset and rename for the driver
fj.Get(merged, `@project:{"pick":["host","port"],"rename":{"port":"dbPort"}}`).Raw()
// → {"host":"localhost","dbPort":5433}Example 7 — Leaderboard: zip parallel arrays, filter, and pluck
// Two parallel arrays zipped via @group into an array-of-objects, then filtered and plucked
leaderboard := `{"player":["Alice","Bob","Carol","Dave"],"score":[98,72,85,91]}`
// Zip the parallel arrays into objects
grouped := fj.Get(leaderboard, `@group`).Raw()
// → [{"player":"Alice","score":98},{"player":"Bob","score":72},
// {"player":"Carol","score":85},{"player":"Dave","score":91}]
// Players with a score of 85 or above
fj.Get(grouped, `@filter:{"key":"score","op":"gte","value":85}|@pluck:player`).Raw()
// → ["Alice","Carol","Dave"]
// Top player's full record
fj.Get(grouped, `@filter:{"key":"score","op":"gte","value":95}|@first`).Raw()
// → {"player":"Alice","score":98}func init() {
fj.AddTransformer("redact", fj.TransformerFunc(func(json, arg string) string {
return `"[REDACTED]"`
}))
}
// Usage in path
fj.Get(json, "user.password.@redact").String() // "[REDACTED]"Transformers can be disabled globally with fj.DisableTransformers = true.
// Full-tree substring search across all leaf values
hits := response.SearchJSONBody("admin")
// Wildcard scan of leaf values
hits = response.SearchJSONBodyMatch("err*")
// Find all values stored under specific key names
emails := response.SearchJSONBodyByKey("email")
// Find all values under keys matching a wildcard
hits = response.SearchJSONBodyByKeyPattern("user*")
// Substring / wildcard check at a specific path
response.JSONBodyContains("user.role", "admin")
response.JSONBodyContainsMatch("user.email", "*@example.com")
// Return the dot-notation path where a value first appears
path := response.FindJSONBodyPath("alice@example.com")
// All paths where value matches a pattern
paths := response.FindJSONBodyPathsMatch("err*")import "github.com/sivaosorg/replify/pkg/fj"
// Count elements at a path
n := response.CountJSONBody("items")
// Filter array elements by predicate
active := response.FilterJSONBody("users", func(ctx fj.Context) bool {
return ctx.Get("active").Bool()
})
// First match
admin := response.FirstJSONBody("users", func(ctx fj.Context) bool {
return ctx.Get("role").String() == "admin"
})
// Deduplicate (first-occurrence order preserved)
tags := response.DistinctJSONBody("tags")
// Project fields from an array of objects
rows := response.PluckJSONBody("users", "id", "email")
// Group by a key field
byRole := response.GroupByJSONBody("users", "role")
// Sort array by a field (numeric or string comparison)
sorted := response.SortJSONBody("products", "price", true)- Read-only:
fjcannot write or modify JSON. Useencoding/jsonfor serialization. - No schema validation: For strict schema enforcement use a dedicated library.
- No struct binding:
fjreturnsContextvalues, not typed Go structs. Useencoding/jsonwhen binding is required. Raw()lifetime: The raw string returned byContext.Raw()is a zero-copy view into the source JSON string. It must not outlive the original string.UnsafeBytes: The byte slice returned byfj.UnsafeBytesshares memory with the source string. Never mutate it, as this violates Go's string immutability guarantees and can cause undefined behavior.- Malformed input:
fjdoes not validate JSON before parsing. Pass untrusted input throughfj.IsValidJSONorValidJSONBody()first. - Transformers are global:
AddTransformerwrites to a package-level registry. Register all transformers during program initialization (e.g., ininit()functions) before concurrent access begins to avoid data races.
-
Check existence before use
if ctx := response.QueryJSONBody("optional.key"); ctx.Exists() { process(ctx.String()) }
-
Parse once, query many times
doc := response.JSONBodyParser() id := doc.Get("user.id").String() email := doc.Get("user.email").String() role := doc.Get("user.role").String()
-
Prefer
GetBytesfor byte-slice payloads// ✅ avoids string conversion allocation ctx := fj.GetBytes(rawBytes, "user.name") // ❌ unnecessary allocation ctx = fj.Get(string(rawBytes), "user.name")
-
Validate untrusted input first
if !response.ValidJSONBody() { return errors.New("invalid JSON body") }
-
Register custom transformers in
init()func init() { fj.AddTransformer("mask", func(json, arg string) string { return `"***"` }) }
-
Never mutate
UnsafeBytesoutputb := fj.UnsafeBytes(someString) // ✅ read-only access _ = b[0] // ❌ mutating b corrupts the original string
To contribute to this project, follow these steps:
-
Clone the repository
git clone --depth 1 https://github.com/sivaosorg/replify.git
-
Navigate to the project directory
cd replify -
Prepare the project environment
go mod tidy
-
Make your changes
- Follow Go best practices
- Add tests for new features
- Update documentation
-
Run tests
go test ./... -
Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
Part of the replify ecosystem:
- replify - API response wrapping library (this package)
- conv - Type conversion utilities
- coll - Type-safe collection utilities
- common - Reflection-based utilities
- encoding - JSON encoding utilities
- hashy - Deterministic hashing
- match - Wildcard pattern matching
- msort - Map sorting utilities
- randn - Random data generation
- ref - Pointer utilities
- strutil - String utilities
- truncate - String truncation utilities
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Built with ❤️ for the Go community.