jsonrpc

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 16 Imported by: 0

README

jsonrpc

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

jsonrpc is a transport-neutral, full JSON-RPC 2.0 server and client package. Protocol behavior is explicit, errors are auditable, middleware is composable, HTTP is optional, and malformed input is conformance- and fuzz-tested.

Status

The package has a stable v1 API and wire contract. Production package code is held to meaningful 100% statement coverage.

Requirements

  • Go 1.26.6 or later
  • no runtime dependencies outside the standard library

Installation

go get github.com/faustbrian/go-jsonrpc

Quickstart

registry := jsonrpc.NewRegistry()
err := registry.Register("math.add", func(
    ctx context.Context,
    params json.RawMessage,
) (any, error) {
    values, rpcErr := jsonrpc.DecodeParams[[]int](params)
    if rpcErr != nil || len(values) != 2 {
        return nil, jsonrpc.InvalidParams()
    }

    return values[0] + values[1], nil
})
if err != nil {
    return err
}

handler := jsonrpc.NewHTTPHandler(jsonrpc.NewDispatcher(registry))

Trusted protocol adapters can register reserved rpc.* methods explicitly with Registry.RegisterSystem; ordinary application registration continues to reject that namespace.

Use NewClient with NewHTTPTransport for client calls. The quickstart contains complete server, client, notification, and batch examples.

Package Guarantees

  • requests, notifications, and explicit null IDs remain distinct
  • string, number, and null IDs round-trip without coercion
  • standard errors use the required codes and response shapes
  • batch and notification-only behavior follows JSON-RPC 2.0
  • clients validate response shape, ID correlation, duplicates, and missing batch members
  • dispatcher and client parsing are independently resource-bounded
  • protocol dispatch remains transport-neutral
  • adapters can use Dispatcher.DispatchSingle to apply a compatible custom response envelope without decoding an already encoded dispatcher response

Documentation

Start with the documentation index, quickstart, adoption guide, and API reference. Use the conformance matrix, middleware guide, hardening report, and specification decision register for production review.

AI tools can use llms.txt and llms-full.txt. Release history is maintained in CHANGELOG.md. Runnable programs live under examples.

Development

Run make check before submitting a change. This enforces formatting, static analysis, race tests, meaningful 100% coverage, fuzz smoke, benchmarks, documentation, and vulnerability scanning.

Contributing

Read CONTRIBUTING.md and follow the code of conduct. Protocol and public API changes require explicit compatibility analysis.

Security

Report vulnerabilities privately according to SECURITY.md. Review docs/security.md before exposing a dispatcher to untrusted clients.

License

jsonrpc is available under the MIT License. Attribution and third-party policy are recorded in NOTICE and THIRD_PARTY_NOTICES.md.

Ecosystem

Use the Golib documentation portal to choose companion packages, supported stacks, recipes, and operations guidance.

Documentation

Overview

Package jsonrpc implements transport-neutral JSON-RPC 2.0 clients and servers. It provides explicit protocol envelopes, request dispatch, middleware, structured errors, strict client response validation, and thin net/http adapters without imposing a router, logger, tracer, or validator.

Requests and responses can be processed directly as bytes through Dispatcher and Transport, allowing custom transports to share the same protocol rules. The protocol is defined by the JSON-RPC 2.0 specification: https://www.jsonrpc.org/specification.

See the repository documentation for compatibility guarantees and complete server, client, notification, and batch examples.

Example
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http/httptest"

	jsonrpc "github.com/faustbrian/go-jsonrpc"
)

func main() {
	registry := jsonrpc.NewRegistry()
	_ = registry.Register("add", func(_ context.Context, raw json.RawMessage) (any, error) {
		values, rpcErr := jsonrpc.DecodeParams[[]int](raw)
		if rpcErr != nil || len(values) != 2 {
			return nil, jsonrpc.InvalidParams()
		}
		return values[0] + values[1], nil
	})
	server := httptest.NewServer(jsonrpc.NewHTTPHandler(jsonrpc.NewDispatcher(registry)))
	defer server.Close()
	transport, _ := jsonrpc.NewHTTPTransport(server.URL)
	result, _ := jsonrpc.Call[int](context.Background(), jsonrpc.NewClient(transport), "add", []int{2, 3})
	fmt.Println(result)
}
Output:
5

Index

Examples

Constants

View Source
const (
	// CodeRequestLimitExceeded is the implementation-defined server error used
	// when a dispatcher payload or batch exceeds its configured bound.
	CodeRequestLimitExceeded = -32000
	// CodeParseError indicates invalid JSON.
	CodeParseError = -32700
	// CodeInvalidRequest indicates a structurally invalid request.
	CodeInvalidRequest = -32600
	// CodeMethodNotFound indicates that no handler is registered for a method.
	CodeMethodNotFound = -32601
	// CodeInvalidParams indicates invalid method parameters.
	CodeInvalidParams = -32602
	// CodeInternalError indicates an internal JSON-RPC failure.
	CodeInternalError = -32603
)
View Source
const Version = "2.0"

Version is the JSON-RPC protocol version implemented by this package.

Variables

View Source
var (
	ErrTransport              = errors.New("jsonrpc: transport error")
	ErrInvalidResponse        = errors.New("jsonrpc: invalid response")
	ErrMismatchedID           = errors.New("jsonrpc: mismatched response id")
	ErrUnexpectedResponse     = errors.New("jsonrpc: unexpected response")
	ErrMissingResponse        = errors.New("jsonrpc: missing batch response")
	ErrDuplicateResponse      = errors.New("jsonrpc: duplicate batch response")
	ErrDuplicateRequestID     = errors.New("jsonrpc: duplicate batch request id")
	ErrEmptyBatch             = errors.New("jsonrpc: empty client batch")
	ErrClientResponseTooLarge = errors.New("jsonrpc: client response too large")
)

Client errors identify transport, envelope, correlation, batch, and size failures and support errors.Is through direct return or wrapping.

View Source
var (
	ErrHTTPStatus       = errors.New("jsonrpc: unexpected HTTP status")
	ErrHTTPContentType  = errors.New("jsonrpc: invalid HTTP response content type")
	ErrResponseTooLarge = errors.New("jsonrpc: HTTP response too large")
)

HTTP transport errors distinguish status, content-type, and body-limit failures and support errors.Is through direct return or wrapping.

View Source
var (
	ErrInvalidMethodName       = errors.New("jsonrpc: invalid method name")
	ErrMethodAlreadyRegistered = errors.New("jsonrpc: method already registered")
	ErrNilHandler              = errors.New("jsonrpc: nil handler")
)

Registration errors identify reserved method names, duplicate methods, and nil handlers.

Functions

func Call

func Call[T any](ctx context.Context, client *Client, method string, params any) (T, error)

Call sends one request and returns its result decoded as T.

func IsJSONContentType

func IsJSONContentType(value string) bool

IsJSONContentType reports whether value is application/json, application/json-rpc, or an application subtype ending in +json.

Types

type AtomicIDGenerator

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

AtomicIDGenerator generates concurrency-safe, monotonically increasing numeric IDs.

func NewAtomicIDGenerator

func NewAtomicIDGenerator(start int64) *AtomicIDGenerator

NewAtomicIDGenerator creates a generator whose first ID is start+1.

func (*AtomicIDGenerator) NextID

func (generator *AtomicIDGenerator) NextID() ID

NextID atomically increments the generator and returns the new numeric ID.

type BatchCall

type BatchCall struct {
	// Method is the JSON-RPC method name.
	Method string
	// Params must encode as an object or array when non-nil.
	Params any
	// Result receives a successful response when non-nil.
	Result any
	// Notification omits the request ID and expects no response member.
	Notification bool
	// Error receives a valid JSON-RPC failure response.
	Error *Error
	// contains filtered or unexported fields
}

BatchCall describes one member of a client batch. Batch writes a decoded success into Result and assigns a protocol failure to Error.

type Client

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

Client validates requests and correlates responses over a Transport. Its default AtomicIDGenerator is safe for concurrent calls; custom transports, generators, and BatchCall values retain their own concurrency contracts.

func NewClient

func NewClient(transport Transport, options ...ClientOption) *Client

NewClient constructs a client. A nil transport is reported as ErrTransport when an operation is attempted. Nil options are ignored.

func (*Client) Batch

func (client *Client) Batch(ctx context.Context, calls ...*BatchCall) error

Batch sends calls together and correlates every non-notification response by ID. It rejects empty batches, nil calls, duplicate generated IDs, and malformed response membership.

Example
package main

import (
	"context"
	"fmt"

	jsonrpc "github.com/faustbrian/go-jsonrpc"
)

func main() {
	transport := jsonrpc.TransportFunc(func(context.Context, []byte) ([]byte, error) {
		return []byte(`[
			{"jsonrpc":"2.0","result":4,"id":2},
			{"jsonrpc":"2.0","result":2,"id":1}
		]`), nil
	})
	client := jsonrpc.NewClient(transport)
	var first, second int
	_ = client.Batch(context.Background(),
		&jsonrpc.BatchCall{Method: "double", Params: []int{1}, Result: &first},
		&jsonrpc.BatchCall{Method: "double", Params: []int{2}, Result: &second},
	)
	fmt.Println(first, second)
}
Output:
2 4

func (*Client) Call

func (client *Client) Call(ctx context.Context, method string, params, result any) error

Call sends one request, validates and correlates its response, and decodes a successful result into result when result is non-nil.

func (*Client) Notify

func (client *Client) Notify(ctx context.Context, method string, params any) error

Notify sends a notification and requires the peer to return no payload.

type ClientOption

type ClientOption func(*Client)

ClientOption configures a Client during construction.

func WithIDGenerator

func WithIDGenerator(generator IDGenerator) ClientOption

WithIDGenerator installs a non-nil request ID generator.

func WithMaxClientResponseBytes

func WithMaxClientResponseBytes(limit int64) ClientOption

WithMaxClientResponseBytes changes the client's four-MiB reply parsing limit.

type Dispatcher

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

Dispatcher validates and executes JSON-RPC requests and batches. Its configuration is immutable after construction; its Registry may be updated concurrently.

func NewDispatcher

func NewDispatcher(registry *Registry, options ...DispatcherOption) *Dispatcher

NewDispatcher constructs a bounded dispatcher. A nil registry is replaced by an empty registry, and nil options are ignored.

func (*Dispatcher) Dispatch

func (d *Dispatcher) Dispatch(ctx context.Context, payload []byte) ([]byte, bool)

Dispatch processes one JSON-RPC message. The boolean reports whether the caller must send the returned response; notifications intentionally return no response.

Notification and batch behavior follow:

func (*Dispatcher) DispatchSingle

func (d *Dispatcher) DispatchSingle(
	ctx context.Context,
	payload []byte,
) (Response, bool)

DispatchSingle processes one non-batch JSON-RPC message and returns its typed response before wire encoding. The boolean is false for notifications.

type DispatcherOption

type DispatcherOption func(*Dispatcher)

DispatcherOption configures a Dispatcher during construction.

func WithErrorMapper

func WithErrorMapper(mapper ErrorMapper) DispatcherOption

WithErrorMapper replaces the default internal-error mapper. Returning nil or an invalid error is contained as an internal error.

func WithHooks

func WithHooks(hooks Hooks) DispatcherOption

WithHooks installs lifecycle observers. Hook panics are contained and hook mutations cannot change protocol output.

func WithMaxBatchItems

func WithMaxBatchItems(limit int) DispatcherOption

WithMaxBatchItems changes the dispatcher's default limit of 1,024 members.

func WithMaxDispatchBytes

func WithMaxDispatchBytes(limit int64) DispatcherOption

WithMaxDispatchBytes changes the dispatcher's four-MiB payload limit.

func WithMaxNestingDepth

func WithMaxNestingDepth(limit int) DispatcherOption

WithMaxNestingDepth changes the maximum number of nested JSON arrays and objects accepted before dispatch. The default matches encoding/json's built-in maximum depth of 10,000.

func WithMiddleware

func WithMiddleware(middleware ...Middleware) DispatcherOption

WithMiddleware appends middleware in outermost-to-innermost order.

Example
package main

import (
	"context"
	"encoding/json"
	"fmt"

	jsonrpc "github.com/faustbrian/go-jsonrpc"
)

func main() {
	registry := jsonrpc.NewRegistry()
	_ = registry.Register("ping", func(context.Context, json.RawMessage) (any, error) {
		return "pong", nil
	})
	logging := func(next jsonrpc.Handler) jsonrpc.Handler {
		return func(ctx context.Context, params json.RawMessage) (any, error) {
			request, _ := jsonrpc.RequestFromContext(ctx)
			fmt.Println(request.Method)
			return next(ctx, params)
		}
	}
	dispatcher := jsonrpc.NewDispatcher(registry, jsonrpc.WithMiddleware(logging))
	dispatcher.Dispatch(context.Background(), []byte(`{"jsonrpc":"2.0","method":"ping","id":1}`))
}
Output:
ping

type Error

type Error struct {
	// Code is the integer JSON-RPC error code.
	Code int `json:"code"`
	// Message is the public error description.
	Message string `json:"message"`
	// Data contains optional public JSON details.
	Data json.RawMessage `json:"data,omitempty"`
	// contains filtered or unexported fields
}

Error is a JSON-RPC error object. Cause is retained locally and is never serialized, allowing callers to preserve diagnostic context safely. See https://www.jsonrpc.org/specification#error_object.

func DecodeParams

func DecodeParams[T any](params json.RawMessage) (T, *Error)

DecodeParams strictly decodes params as T. Duplicate or unknown named members, malformed JSON, and trailing data return InvalidParams.

func InternalError

func InternalError() *Error

InternalError constructs the standard internal error.

func InvalidParams

func InvalidParams() *Error

InvalidParams constructs the standard invalid-params error.

func InvalidRequest

func InvalidRequest() *Error

InvalidRequest constructs the standard invalid-request error.

func MethodNotFound

func MethodNotFound() *Error

MethodNotFound constructs the standard method-not-found error.

func NewError

func NewError(code int, message string) *Error

NewError constructs a JSON-RPC error with code and public message. Application codes should avoid the protocol-reserved range -32768 through -32000.

func ParseError

func ParseError() *Error

ParseError constructs the standard parse error.

func RequestLimitExceeded

func RequestLimitExceeded() *Error

RequestLimitExceeded constructs the dispatcher resource-limit error.

func (*Error) Error

func (e *Error) Error() string

Error returns a textual representation containing the public code and message.

func (*Error) UnmarshalJSON

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

UnmarshalJSON decodes a strict JSON-RPC error object.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the local cause, which is never serialized.

func (*Error) WithCause

func (e *Error) WithCause(cause error) *Error

WithCause retains a local cause without exposing it on the wire.

func (*Error) WithData

func (e *Error) WithData(value any) *Error

WithData JSON-encodes public error details. An encoding failure clears Data and is retained as the local cause.

type ErrorMapper

type ErrorMapper func(error) *Error

ErrorMapper converts an application error into safe public JSON-RPC data.

type HTTPHandler

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

HTTPHandler adapts a Dispatcher to net/http with strict POST, content-type, and request-body handling.

func NewHTTPHandler

func NewHTTPHandler(dispatcher *Dispatcher, options ...HTTPHandlerOption) *HTTPHandler

NewHTTPHandler constructs an HTTP handler. A nil dispatcher is replaced by an empty dispatcher, and nil options are ignored.

func (*HTTPHandler) ServeHTTP

func (h *HTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request)

ServeHTTP validates and dispatches one HTTP JSON-RPC request.

type HTTPHandlerOption

type HTTPHandlerOption func(*HTTPHandler)

HTTPHandlerOption configures an HTTPHandler during construction.

func WithMaxRequestBytes

func WithMaxRequestBytes(limit int64) HTTPHandlerOption

WithMaxRequestBytes changes the default four-MiB HTTP request-body limit.

type HTTPStatusError

type HTTPStatusError struct {
	// StatusCode is the peer's HTTP response status.
	StatusCode int
	// Body is the trimmed, bounded response body.
	Body string
}

HTTPStatusError reports a non-200 HTTP response and its bounded body.

func (*HTTPStatusError) Error

func (err *HTTPStatusError) Error() string

Error returns the status code and, when present, response body.

func (*HTTPStatusError) Unwrap

func (err *HTTPStatusError) Unwrap() error

Unwrap returns ErrHTTPStatus.

type HTTPTransport

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

HTTPTransport exchanges JSON-RPC payloads over HTTP POST.

func NewHTTPTransport

func NewHTTPTransport(endpoint string, options ...HTTPTransportOption) (*HTTPTransport, error)

NewHTTPTransport validates an HTTP(S) endpoint and constructs a transport. The default client does not follow redirects. Nil options are ignored.

func (*HTTPTransport) RoundTrip

func (transport *HTTPTransport) RoundTrip(ctx context.Context, payload []byte) ([]byte, error)

RoundTrip posts payload and returns a bounded JSON response. A 204 response returns a nil payload, and non-200 statuses return *HTTPStatusError.

type HTTPTransportOption

type HTTPTransportOption func(*HTTPTransport)

HTTPTransportOption configures an HTTPTransport during construction.

func WithHTTPClient

func WithHTTPClient(client *http.Client) HTTPTransportOption

WithHTTPClient installs a non-nil HTTP client. Its timeout and redirect policy remain caller-owned.

func WithHTTPHeader

func WithHTTPHeader(name, value string) HTTPTransportOption

WithHTTPHeader adds a header to each request. Content-Type and Accept are always overwritten with JSON values during RoundTrip.

func WithMaxResponseBytes

func WithMaxResponseBytes(limit int64) HTTPTransportOption

WithMaxResponseBytes changes the default four-MiB HTTP response-body limit.

type Handler

type Handler func(context.Context, json.RawMessage) (any, error)

Handler implements one JSON-RPC method.

type Hooks

type Hooks struct {
	// OnRequest observes a copied validated request or nil for an invalid
	// envelope and may return a derived context for subsequent processing.
	OnRequest func(context.Context, *Request) context.Context
	// OnResponse observes a copied internal outcome, including notifications.
	OnResponse func(context.Context, *Request, *Response)
}

Hooks observe the complete dispatcher lifecycle, including protocol errors that occur before a Handler or Middleware can run. A nil Request represents an unparseable or invalid request. Notifications provide an internal outcome to OnResponse even though that response is never placed on the wire.

type ID

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

ID preserves the exact JSON representation of a string, number, or null ID. See https://www.jsonrpc.org/specification#request_object.

func NullID

func NullID() ID

NullID constructs an explicit null ID.

func NumberID

func NumberID(value json.Number) ID

NumberID constructs a numeric ID while preserving value's wire spelling.

func StringID

func StringID(value string) ID

StringID constructs a string ID. Invalid UTF-8 is replaced according to encoding/json rules so correlation matches the transmitted value.

func (ID) Equal

func (id ID) Equal(other ID) bool

Equal reports whether IDs have the same kind and value. Mathematically equivalent numeric spellings compare equal.

func (ID) Kind

func (id ID) Kind() IDKind

Kind returns the ID's representation kind.

func (ID) MarshalJSON

func (id ID) MarshalJSON() ([]byte, error)

MarshalJSON preserves the ID's original JSON spelling. A missing ID marshals as null when encoded outside a Request.

func (*ID) UnmarshalJSON

func (id *ID) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a string, number, or null ID without numeric precision loss and rejects invalid UTF-8.

type IDGenerator

type IDGenerator interface {
	NextID() ID
}

IDGenerator supplies request IDs to a Client.

type IDKind

type IDKind uint8

IDKind identifies the JSON representation of an ID.

const (
	// IDMissing represents an absent ID member and therefore a notification.
	IDMissing IDKind = iota
	// IDString represents a JSON string ID.
	IDString
	// IDNumber represents a JSON number ID.
	IDNumber
	// IDNull represents an explicit JSON null ID.
	IDNull
)

type Middleware

type Middleware func(Handler) Handler

Middleware wraps a Handler with cross-cutting behavior.

type Registry

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

Registry is a concurrency-safe method registry. Its zero value is ready for use. A Registry must not be copied after first use.

func NewRegistry

func NewRegistry() *Registry

NewRegistry constructs an empty registry.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (Handler, bool)

Lookup returns the handler registered under name.

func (*Registry) Register

func (r *Registry) Register(name string, handler Handler) error

Register adds handler under name. Names beginning with rpc. are reserved, and an existing name cannot be replaced.

func (*Registry) RegisterSystem

func (r *Registry) RegisterSystem(name string, handler Handler) error

RegisterSystem adds an explicitly reserved rpc.* handler. This method is intended for protocol extensions such as OpenRPC's rpc.discover and rejects application method names.

type Request

type Request struct {
	// JSONRPC must equal Version.
	JSONRPC string `json:"jsonrpc"`
	// Method is the case-sensitive method name.
	Method string `json:"method"`
	// Params contains an optional JSON object or array.
	Params json.RawMessage `json:"params,omitempty"`
	// ID identifies a request; it is missing for notifications.
	ID ID `json:"-"`
	// contains filtered or unexported fields
}

Request represents the protocol's request object. Params, when present, must follow https://www.jsonrpc.org/specification#parameter_structures. See https://www.jsonrpc.org/specification#request_object.

func NewNotification

func NewNotification(method string, params any) (Request, error)

NewNotification constructs a validated request without an ID.

func NewRequest

func NewRequest(method string, params any, id ID) (Request, error)

NewRequest constructs a validated request with an explicit non-missing ID.

func RequestFromContext

func RequestFromContext(ctx context.Context) (Request, bool)

RequestFromContext returns the validated request made available to the active middleware or handler.

func (Request) IsNotification

func (r Request) IsNotification() bool

IsNotification reports whether the request omitted its ID member as defined by https://www.jsonrpc.org/specification#notification.

func (Request) MarshalJSON

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

MarshalJSON encodes a request while preserving notification ID omission.

func (*Request) UnmarshalJSON

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

UnmarshalJSON decodes a request while preserving whether ID and method were present and rejecting ambiguous protocol members.

func (Request) Validate

func (r Request) Validate() *Error

Validate returns an InvalidRequest error when the request envelope violates JSON-RPC 2.0.

type Response

type Response struct {
	// JSONRPC must equal Version.
	JSONRPC string `json:"jsonrpc"`
	// Result contains the success value.
	Result json.RawMessage `json:"result,omitempty"`
	// Error contains the failure object.
	Error *Error `json:"error,omitempty"`
	// ID correlates the response with its request.
	ID ID `json:"id"`
	// contains filtered or unexported fields
}

Response represents the object defined by https://www.jsonrpc.org/specification#response_object.

func (Response) MarshalJSON

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

MarshalJSON encodes exactly one result or error member.

func (*Response) UnmarshalJSON

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

UnmarshalJSON decodes a response and records the presence of result, error, and ID members for later validation.

func (Response) Validate

func (r Response) Validate() error

Validate checks the version, ID, result/error exclusivity, and error shape.

type Transport

type Transport interface {
	RoundTrip(context.Context, []byte) ([]byte, error)
}

Transport exchanges one complete JSON-RPC payload with a peer.

type TransportFunc

type TransportFunc func(context.Context, []byte) ([]byte, error)

TransportFunc adapts a function to Transport.

func (TransportFunc) RoundTrip

func (function TransportFunc) RoundTrip(ctx context.Context, payload []byte) ([]byte, error)

RoundTrip calls function with ctx and payload.

Directories

Path Synopsis
cmd
semvercheck command
examples
client command
e2e command
server command
internal

Jump to

Keyboard shortcuts

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