jsonrpc

package module
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2025 License: MIT Imports: 10 Imported by: 1

README

jsonrpc Go Documentation MIT License

A JSON-RPC 2.0 implementation in Go.

Utilizes the bytedance/sonic library for JSON serialization.

Attempts to conform fully to the JSON-RPC 2.0 Specification, with a few minor exceptions:

  • The id field is allowed to be fractional numbers, in addition to integers and strings. The specification notes that "numbers should not contain fractional parts", but this library allows them for convenience.
  • Error handling
    • Error codes can be zero if a message is provided (the specification allows any integer).
    • The error field is flexible in unmarshaling, with fallback logic to handle various error formats for custom error handling downstream.

Install

go get github.com/jkbrsn/jsonrpc

Usage

Single Requests and Responses
Creating and Encoding a Request
// Create a request with auto-generated ID
req := jsonrpc.NewRequest("sum", []any{1, 2})

// Or create with a specific ID
req := jsonrpc.NewRequestWithID("sum", []any{1, 2}, "my-id")

// Encode to JSON
data, err := req.MarshalJSON()
Decoding and Handling a Response
// Decode a response from JSON
resp, err := jsonrpc.DecodeResponse(data)
if err != nil {
    // Handle decode error
}

// Check for JSON-RPC error
if resp.Err() != nil {
    fmt.Printf("RPC Error: %s\n", resp.Err().Message)
    return
}

// Unmarshal the result into your type
var result int
if err := resp.UnmarshalResult(&result); err != nil {
    // Handle unmarshal error
}
fmt.Printf("Result: %d\n", result)
Creating a Notification
// Notifications are requests without IDs (no response expected)
notification := jsonrpc.NewNotification("log", map[string]any{
    "level": "info",
    "message": "Operation completed",
})
Working with Params

The library supports both positional (array) and named (object) parameters, as well as structured parameter unmarshaling.

Positional Parameters
// Create a request with array parameters
req := jsonrpc.NewRequest("subtract", []any{42, 23})
// Params: [42, 23]
Named Parameters
// Create a request with object parameters
req := jsonrpc.NewRequest("updateUser", map[string]any{
    "userId": 123,
    "name":   "Alice",
    "active": true,
})
// Params: {"userId": 123, "name": "Alice", "active": true}
Unmarshaling Params into Structs
// Define your parameter structure
type UserParams struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

// Unmarshal params into the struct
var params UserParams
if err := req.UnmarshalParams(&params); err != nil {
    // Handle error
}
// Use params.Name, params.Email, params.Age
Batch Requests and Responses

The library supports JSON-RPC 2.0 batch operations for sending multiple requests or responses in a single call.

Encoding a Batch Request
reqs := []*jsonrpc.Request{
    jsonrpc.NewRequest("sum", []any{1, 2}),
    jsonrpc.NewRequest("subtract", []any{5, 3}),
}
data, err := jsonrpc.EncodeBatchRequest(reqs)

// Or use the helper:
reqs, err := jsonrpc.NewBatchRequest(
    []string{"sum", "subtract"},
    []any{[]any{1, 2}, []any{5, 3}},
)
Decoding a Batch Response
resps, err := jsonrpc.DecodeBatchResponse(data)
for _, resp := range resps {
    if resp.Err() != nil {
        // Handle error
    } else {
        var result int
        resp.UnmarshalResult(&result)
    }
}
Auto-detecting Single vs Batch
resps, isBatch, err := jsonrpc.DecodeResponseOrBatch(data)
if isBatch {
    fmt.Printf("Received batch with %d responses\n", len(resps))
} else {
    fmt.Println("Received single response")
}
Notifications in Batches

Batches can contain notifications (requests without IDs). The server should not send responses for notifications:

reqs, err := jsonrpc.NewBatchNotification(
    []string{"log", "notify"},
    []any{map[string]any{"level": "info"}, map[string]any{"message": "test"}},
)

Release Process

See release.yml for the release process specifics.

Release Workflow Behavior

The Manual Release workflow enforces consistent versioning rules:

  • version = 1.5.0, prerelease = true → creates v1.5.0-rc.1 (or the next -rc.N if others exist).
  • version = 1.5.0-rc.7, prerelease = true → creates exactly v1.5.0-rc.7 (if not already tagged).
  • version = v1.5.0, prerelease = false → creates final v1.5.0 (allowed even if prereleases exist).
  • If final v1.5.0 already exists → both prerelease = true and final runs for 1.5.0 are blocked.
  • Base version bumping → the base X.Y.Z must always be strictly greater than the latest existing final tag in the repo.

This means you can iterate with prerelease = true and later “promote” the same base to a final, but you cannot reuse or downgrade existing finals.

Migration Guide

If you're upgrading from earlier versions, some function names have changed to follow Go conventions more closely. See MIGRATION.md for detailed migration instructions.

Quick reference:

  • RequestFromBytesDecodeRequest
  • NewResponseFromBytesDecodeResponse
  • NewResponseFromStreamDecodeResponseFromReader (note: does not auto-close reader)
  • resp.IDRaw()resp.IDOrNil()
  • resp.Errorresp.Err() (field is now unexported)
  • resp.Resultresp.RawResult() (field is now unexported)
  • resp.JSONRPCresp.Version() (field is now unexported)
  • resp.IDresp.IDOrNil() (field is now unexported)

Contributing

For contributions, please open a GitHub issue with your questions and suggestions. Before submitting an issue, have a look at the existing TODO list to see if your idea is already in the works.

Documentation

Overview

Package jsonrpc provides a Go implementation of the JSON-RPC 2.0 specification, as well as tools to parse and work with JSON-RPC requests and responses.

Index

Examples

Constants

View Source
const (
	InvalidRequest      = -32600
	MethodNotFound      = -32601
	InvalidParams       = -32602
	ServerSideException = -32603
	ParseError          = -32700
)

JSON-RPC error codes

Variables

This section is empty.

Functions

func EncodeBatchRequest

func EncodeBatchRequest(reqs []*Request) ([]byte, error)

EncodeBatchRequest marshals a slice of JSON-RPC requests into a batch (JSON array). Returns an error if: - Input slice is empty - Any request fails validation

func EncodeBatchResponse

func EncodeBatchResponse(resps []*Response) ([]byte, error)

EncodeBatchResponse marshals a slice of JSON-RPC responses into a batch (JSON array). Returns an error if: - Input slice is empty - Any response fails validation

func RandomJSONRPCID

func RandomJSONRPCID() int64

RandomJSONRPCID returns a randomly generated value appropriate for a JSON-RPC ID field. Returns an int64 in the range [0, 2147483647] (int32 range) for compatibility. Uses math/rand/v2 which is automatically seeded and provides good randomness.

Types

type Error

type Error struct {
	Code    int    `json:"code,omitempty"`
	Message string `json:"message,omitempty"`
	Data    any    `json:"data,omitempty"` // Optional data field
}

Error represents a JSON-RPC error.

Example (Data)

ExampleError_data demonstrates using the Data field in errors.

// Create an error with additional data
err := &Error{
	Code:    -32602,
	Message: "Invalid params",
	Data: map[string]any{
		"field":  "email",
		"reason": "invalid format",
	},
}

fmt.Printf("Code: %d\n", err.Code)
fmt.Printf("Message: %s\n", err.Message)
fmt.Printf("Data type: %T\n", err.Data)
Output:
Code: -32602
Message: Invalid params
Data type: map[string]interface {}

func (*Error) Equals

func (e *Error) Equals(other *Error) bool

Equals compares the contents of two JSON-RPC errors for equality. Returns true if both errors have the same Code and Message.

Note: The Data field is intentionally excluded from comparison because:

  1. Data has type `any`, making deep comparison complex and expensive
  2. Error equality is typically determined by code + message alone
  3. Data is optional and used for supplementary information

If you need to compare Data fields, do so separately after calling Equals().

func (*Error) IsEmpty

func (e *Error) IsEmpty() bool

IsEmpty returns true if the error is empty, which is if the error is nil or both code and message are empty.

Note: Zero error codes are valid per JSON-RPC 2.0 spec, but are treated as "empty" when both code=0 and message="". This helps identify placeholder or uninitialized errors.

func (*Error) String

func (e *Error) String() string

String returns a string representation of the error.

func (*Error) UnmarshalJSON

func (e *Error) UnmarshalJSON(data []byte) error

UnmarshalJSON unmarshals an error from a raw JSON-RPC response. The unmarshal logic uses several fallbacks to ensure an error is produced.

func (*Error) Validate

func (e *Error) Validate() error

Validate checks if the error is valid according to the JSON-RPC specification. An error is valid if it has at least one of: a non-zero code or a non-empty message. Zero error codes are allowed per JSON-RPC 2.0 spec, though they're treated as "empty" by IsEmpty() when combined with an empty message.

type Request

type Request struct {
	JSONRPC string `json:"jsonrpc"`
	ID      any    `json:"id,omitempty"`
	Method  string `json:"method"`
	Params  any    `json:"params,omitempty"`
}

Request is a struct for a JSON-RPC request. It conforms to the JSON-RPC 2.0 specification, with minor exceptions. E.g. the ID field is allowed to be fractional in this implementation.

Example (Params_named)

ExampleRequest_params_named demonstrates using named parameters (object).

// Create a request with named parameters
req := NewRequest("updateUser", map[string]any{
	"userId": 123,
	"name":   "Alice",
	"active": true,
})

fmt.Printf("Method: %s\n", req.Method)
fmt.Printf("Params type: %T\n", req.Params)
Output:
Method: updateUser
Params type: map[string]interface {}
Example (Params_nil)

ExampleRequest_params_nil demonstrates a request without parameters.

// Create a request with no parameters
req := NewRequest("getServerTime", nil)

fmt.Printf("Method: %s\n", req.Method)
fmt.Printf("Params: %v\n", req.Params)
Output:
Method: getServerTime
Params: <nil>
Example (Params_positional)

ExampleRequest_params_positional demonstrates using positional parameters (array).

// Create a request with positional parameters
req := NewRequest("subtract", []any{42, 23})

fmt.Printf("Method: %s\n", req.Method)
fmt.Printf("Params type: %T\n", req.Params)
fmt.Printf("Params: %v\n", req.Params)
Output:
Method: subtract
Params type: []interface {}
Params: [42 23]

func DecodeBatchRequest

func DecodeBatchRequest(data []byte) ([]*Request, error)

DecodeBatchRequest parses a JSON-RPC batch request from a byte slice. Returns an error if: - Input is not a JSON array - Array is empty (per JSON-RPC 2.0 spec: "The Server should respond with an error") - Any element fails to parse as a valid Request

func DecodeBatchRequestFromReader

func DecodeBatchRequestFromReader(r io.Reader, expectedSize int) ([]*Request, error)

DecodeBatchRequestFromReader parses a JSON-RPC batch request from an io.Reader.

func DecodeRequest

func DecodeRequest(data []byte) (*Request, error)

DecodeRequest parses a JSON-RPC request from a byte slice.

func DecodeRequestOrBatch

func DecodeRequestOrBatch(data []byte) ([]*Request, bool, error)

DecodeRequestOrBatch attempts to parse either a single request or a batch of requests. Returns (requests, isBatch, error). - For single requests: returns slice with one element, isBatch=false - For batch requests: returns slice with multiple elements, isBatch=true - Empty batches are rejected per JSON-RPC 2.0 spec

func NewBatchNotification

func NewBatchNotification(methods []string, params []any) ([]*Request, error)

NewBatchNotification creates a batch of notifications (requests without IDs).

func NewBatchRequest

func NewBatchRequest(methods []string, params []any) ([]*Request, error)

NewBatchRequest creates a batch of JSON-RPC requests from methods and params. Each request receives an auto-generated ID.

Example

ExampleNewBatchRequest demonstrates creating batch requests with different param types.

methods := []string{"sum", "subtract", "getUser"}
params := []any{
	[]any{1, 2, 3},            // positional params for sum
	[]any{10, 5},              // positional params for subtract
	map[string]any{"id": 123}, // named params for getUser
}

reqs, err := NewBatchRequest(methods, params)
if err != nil {
	fmt.Printf("err: %v\n", err)
	return
}

for i, req := range reqs {
	fmt.Printf("Request %d: %s\n", i, req.Method)
}
Output:
Request 0: sum
Request 1: subtract
Request 2: getUser

func NewNotification

func NewNotification(method string, params any) *Request

NewNotification creates a JSON-RPC 2.0 notification (request without ID).

func NewRequest

func NewRequest(method string, params any) *Request

NewRequest creates a JSON-RPC 2.0 request with an auto-generated ID.

func NewRequestWithID

func NewRequestWithID(method string, params any, id any) *Request

NewRequestWithID creates a JSON-RPC 2.0 request with a specific ID.

func RequestFromBytes

func RequestFromBytes(data []byte) (*Request, error)

RequestFromBytes creates a JSON-RPC request from a byte slice. Deprecated: Use DecodeRequest instead. See MIGRATION.md for details. Will be removed in v2.0.

func (*Request) IDString

func (r *Request) IDString() string

IDString returns the ID as a string.

func (*Request) IsEmpty

func (r *Request) IsEmpty() bool

IsEmpty returns whether the Request can be considered empty. A request is considered empty if the method field is empty.

func (*Request) IsNotification

func (r *Request) IsNotification() bool

IsNotification returns true if this is a notification (no ID expected).

func (*Request) MarshalJSON

func (r *Request) MarshalJSON() ([]byte, error)

MarshalJSON marshals a JSON-RPC request.

func (*Request) String

func (r *Request) String() string

String returns a string representation of the JSON-RPC request. Note: implements the fmt.Stringer interface.

func (*Request) UnmarshalJSON

func (r *Request) UnmarshalJSON(data []byte) error

UnmarshalJSON unmarshals a JSON-RPC request. The function takes two custom actions; sets the JSON-RPC version to 2.0 and unmarshals the ID separately, to handle both string and float64 IDs.

func (*Request) UnmarshalParams

func (r *Request) UnmarshalParams(dst any) error

UnmarshalParams decodes the Params field into the provided destination pointer. This is a convenience method for unmarshaling structured parameters.

Example

ExampleRequest_UnmarshalParams demonstrates unmarshaling params into a struct.

// Create a request with structured params
req := NewRequest("createUser", map[string]any{
	"name":  "Bob",
	"email": "bob@example.com",
	"age":   30,
})

// Define target struct
type UserParams struct {
	Name  string `json:"name"`
	Email string `json:"email"`
	Age   int    `json:"age"`
}

// Unmarshal params into struct
var params UserParams
if err := req.UnmarshalParams(&params); err != nil {
	fmt.Printf("err: %v\n", err)
	return
}

fmt.Printf("Name: %s, Email: %s, Age: %d\n", params.Name, params.Email, params.Age)
Output:
Name: Bob, Email: bob@example.com, Age: 30

func (*Request) Validate

func (r *Request) Validate() error

Validate checks if the JSON-RPC request conforms to the JSON-RPC specification.

type Response

type Response struct {
	// contains filtered or unexported fields
}

Response is a struct for JSON-RPC responses conforming to the JSON-RPC 2.0 specification. Response instances are immutable after decoding and safe for concurrent reads. All fields are unexported to enforce immutability. Use getter methods to access field values.

The Response type uses lazy unmarshaling for the ID and Error fields to optimize performance. These fields are unmarshaled on first access via IDOrNil() or Err() respectively.

func DecodeBatchResponse

func DecodeBatchResponse(data []byte) ([]*Response, error)

DecodeBatchResponse parses a JSON-RPC batch response from a byte slice. Returns an error if: - Input is not a JSON array - Array is empty - Any element fails to parse as a valid Response

func DecodeBatchResponseFromReader

func DecodeBatchResponseFromReader(r io.Reader, expectedSize int) ([]*Response, error)

DecodeBatchResponseFromReader parses a JSON-RPC batch response from an io.Reader.

func DecodeResponse

func DecodeResponse(data []byte) (*Response, error)

DecodeResponse parses and returns a new Response from a byte slice.

func DecodeResponseFromReader

func DecodeResponseFromReader(r io.Reader, expectedSize int) (*Response, error)

DecodeResponseFromReader parses and returns a new Response from an io.Reader. expectedSize is optional and used for internal buffer sizing; pass 0 if unknown.

func DecodeResponseOrBatch

func DecodeResponseOrBatch(data []byte) ([]*Response, bool, error)

DecodeResponseOrBatch attempts to parse either a single response or a batch of responses. Returns (responses, isBatch, error).

func NewErrorResponse

func NewErrorResponse(id any, err *Error) *Response

NewErrorResponse creates a JSON-RPC 2.0 error response.

func NewResponse

func NewResponse(id any, result any) (*Response, error)

NewResponse creates a JSON-RPC 2.0 response with a result.

func NewResponseFromBytes

func NewResponseFromBytes(data []byte) (*Response, error)

NewResponseFromBytes parses and returns a new Response from a byte slice. Deprecated: Use DecodeResponse instead. See MIGRATION.md for details. Will be removed in v2.0.

func NewResponseFromRaw

func NewResponseFromRaw(id any, rawResult json.RawMessage) (*Response, error)

NewResponseFromRaw creates a JSON-RPC 2.0 response with a raw result.

func NewResponseFromStream

func NewResponseFromStream(body io.ReadCloser, expectedSize int) (*Response, error)

NewResponseFromStream parses and returns a new Response from a stream. Deprecated: Use DecodeResponseFromReader instead. Note that DecodeResponseFromReader does NOT automatically close the reader. See MIGRATION.md for details. Will be removed in v2.0.

func (*Response) Equals

func (r *Response) Equals(other *Response) bool

Equals compares the contents of two JSON-RPC responses. This method handles both eagerly and lazily unmarshaled responses by ensuring both IDs and Errors are unmarshaled before comparison.

func (*Response) Err

func (r *Response) Err() *Error

Err returns the error from the response, if any. The error is unmarshaled lazily on first call and cached for subsequent calls. This method is safe for concurrent use.

func (*Response) IDOrNil

func (r *Response) IDOrNil() any

IDOrNil returns the unmarshaled ID, or nil if unmarshaling fails. The ID is unmarshaled lazily on first call and cached for subsequent calls. This method is safe for concurrent use.

func (*Response) IDRaw

func (r *Response) IDRaw() any

IDRaw returns the unmarshaled ID, or nil if unmarshaling fails. Deprecated: Use IDOrNil instead for clearer intent. See MIGRATION.md for details.

func (*Response) IDString

func (r *Response) IDString() string

IDString returns the ID as a string.

func (*Response) IsEmpty

func (r *Response) IsEmpty() bool

IsEmpty returns whether the JSON-RPC response can be considered empty.

This method is primarily used to detect responses that carry no meaningful data, such as responses from notification requests (which shouldn't exist per spec) or placeholder responses.

A response is considered empty when BOTH the error and result are empty:

  • Result is empty if: empty byte slice, null, empty string (""), empty array ([]), empty object ({}), or hex zero value ("0x")
  • Error is empty if: nil, or has both code=0 and message=""

The specific byte pattern checks (null, "0x", etc.) handle common JSON-RPC conventions where these values represent "no data" semantically.

func (*Response) MarshalJSON

func (r *Response) MarshalJSON() ([]byte, error)

MarshalJSON marshals a JSON-RPC response into a byte slice. The public members ID and Error will be prioritized over their raw counterparts.

func (*Response) RawResult

func (r *Response) RawResult() json.RawMessage

RawResult returns the raw JSON-encoded result bytes. For string results, this includes the JSON quotes (e.g., "result" not result). Use UnmarshalResult to decode the result into a specific type.

func (*Response) String

func (r *Response) String() string

String returns a string representation of the JSON-RPC response.

func (*Response) UnmarshalError

func (r *Response) UnmarshalError() error

UnmarshalError unmarshals the raw error into the Error field. The error is unmarshaled lazily on first call and cached for subsequent calls. This method is safe for concurrent use.

Example

ExampleResponse_UnmarshalError demonstrates handling error responses.

jsonData := []byte(
	`{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`,
)
resp, err := DecodeResponse(jsonData)
if err != nil {
	fmt.Printf("Decode error: %v\n", err)
	return
}

// For responses with errors, the error is automatically unmarshaled during decode
// Check if response has an error
if resp.Err() != nil {
	fmt.Printf("RPC Error %d: %s\n", resp.Err().Code, resp.Err().Message)
}
Output:
RPC Error -32601: Method not found

func (*Response) UnmarshalJSON

func (r *Response) UnmarshalJSON(data []byte) error

UnmarshalJSON unmarshals the input data into the members of Response. Note: does not unmarshal the Result field, but leaves that at the caller's discretion (UnmarshalResult). This is an optimization to prevent unnecessary unmarshalling of the Result field for very large blobs.

func (*Response) UnmarshalResult

func (r *Response) UnmarshalResult(dst any) error

UnmarshalResult decodes the raw Result field into the provided destination pointer.

Example

ExampleResponse_UnmarshalResult demonstrates unmarshaling result into different types.

// Simulate decoding a response with a structured result
jsonData := []byte(`{"jsonrpc":"2.0","id":1,"result":{"balance":1000,"currency":"USD"}}`)
resp, err := DecodeResponse(jsonData)
if err != nil {
	fmt.Printf("err: %v\n", err)
	return
}

// Define target struct
type BalanceResult struct {
	Balance  int    `json:"balance"`
	Currency string `json:"currency"`
}

// Unmarshal result
var result BalanceResult
if err := resp.UnmarshalResult(&result); err != nil {
	fmt.Printf("err: %v\n", err)
	return
}

fmt.Printf("Balance: %d %s\n", result.Balance, result.Currency)
Output:
Balance: 1000 USD
Example (Primitives)

ExampleResponse_UnmarshalResult_primitives demonstrates unmarshaling primitive results.

// String result
jsonData := []byte(`{"jsonrpc":"2.0","id":1,"result":"success"}`)
resp, _ := DecodeResponse(jsonData)

var strResult string
if err := resp.UnmarshalResult(&strResult); err != nil {
	fmt.Printf("err: %v\n", err)
	return
}
fmt.Printf("String: %s\n", strResult)

// Number result
jsonData = []byte(`{"jsonrpc":"2.0","id":2,"result":42}`)
resp, _ = DecodeResponse(jsonData)

var intResult int
if err := resp.UnmarshalResult(&intResult); err != nil {
	fmt.Printf("err: %v\n", err)
	return
}
fmt.Printf("Number: %d\n", intResult)

// Boolean result
jsonData = []byte(`{"jsonrpc":"2.0","id":3,"result":true}`)
resp, _ = DecodeResponse(jsonData)

var boolResult bool
if err := resp.UnmarshalResult(&boolResult); err != nil {
	fmt.Printf("err: %v\n", err)
	return
}
fmt.Printf("Boolean: %t\n", boolResult)
Output:
String: success
Number: 42
Boolean: true

func (*Response) Validate

func (r *Response) Validate() error

Validate checks if the JSON-RPC response conforms to the JSON-RPC specification.

func (*Response) Version

func (r *Response) Version() string

Version returns the JSON-RPC protocol version (always "2.0" for valid responses).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL