# How protobuf-go-lite Handles Well-Known Types: Timestamp, Duration, Any, and Empty

> Discover how protobuf-go-lite efficiently handles well-known types like Timestamp Duration Any and Empty using reflection-free implementations and custom ProtoJSON marshaling

- Repository: [Aperture Robotics/protobuf-go-lite](https://github.com/aperturerobotics/protobuf-go-lite)
- Tags: deep-dive
- Published: 2026-02-25

---

**protobuf-go-lite implements Timestamp, Duration, Any, and Empty as lightweight, reflection-free types with custom ProtoJSON marshaling, delegating to RFC-3339 and duration string formats while using an AnyTypeResolver for dynamic type resolution.**

The `aperturerobotics/protobuf-go-lite` repository provides a minimal, high-performance alternative to the standard Go protobuf implementation. Understanding how it handles well-known types (WKTs) is essential for developers migrating from `google.golang.org/protobuf` or building systems that require deterministic JSON serialization without reflection overhead.

## JSON Architecture for Well-Known Types

All four well-known types satisfy the **json.Marshaler** and **json.Unmarshaler** interfaces defined in [`json/marshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/json/marshal.go) and [`json/unmarshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/json/unmarshal.go). This design allows the JSON plugin to treat them like standard messages while providing hand-crafted implementations that avoid reflection.

### ProtoJSON Interface Contracts

The JSON layer defines two core interfaces that every well-known type implements:

```go
type Marshaler interface {
    MarshalProtoJSON(*MarshalState)
}

type Unmarshaler interface {
    UnmarshalProtoJSON(*UnmarshalState)
}

```

These interfaces appear in [`json/marshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/json/marshal.go) and [`json/unmarshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/json/unmarshal.go) respectively. Unlike the standard library's `json.Marshaler`, these methods accept state objects that carry configuration and provide helper methods for writing JSON tokens.

### Configuration and State Management

**MarshalerConfig** and **UnmarshalerConfig** structures carry an `AnyTypeResolver` that the `Any` type uses to look up concrete message constructors. The default configurations leave this resolver nil, causing operations that require type resolution to return `ErrNoAnyTypeResolver`. Users must explicitly provide a resolver when working with `Any` types.

## Timestamp and Duration Implementation

The **Timestamp** and **Duration** types follow nearly identical implementation patterns, differing only in their specific time-formatting helpers.

### Timestamp RFC-3339 Handling

The `Timestamp` type resides in [`types/known/timestamppb/timestamp.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/timestamppb/timestamp.go). It provides construction helpers like `ToTimestamp`, `FromUnixMilli`, and `New` that wrap `time.Time` values.

For JSON serialization, `MarshalProtoJSON` delegates to `MarshalState.WriteTime`:

```go
func (x *Timestamp) MarshalProtoJSON(s *json.MarshalState) {
    if x == nil {
        s.WriteNil()
        return
    }
    s.WriteTime(x.AsTime())
}

```

The `UnmarshalProtoJSON` method uses `ReadTime` to parse RFC-3339 formatted strings back into the internal representation. These helpers live in [`json/state.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/json/state.go) and handle the string formatting according to the protobuf JSON specification.

### Duration String Format Support

The **Duration** type in [`types/known/durationpb/duration.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/durationpb/duration.go) mirrors the Timestamp implementation but uses `ReadDuration` and `WriteDuration` helpers. These methods handle the canonical duration string format (e.g., `"3.5s"`, `"1h30m"`) specified in the protobuf standard.

## Empty Type Handling

The **Empty** well-known type represents a message with no fields. In [`types/known/emptypb/empty.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/emptypb/empty.go), the JSON representation is simply the empty object `{}`.

The implementation is straightforward:

```go
func (x *Empty) MarshalProtoJSON(s *json.MarshalState) {
    s.WriteObjectStart()
    s.WriteObjectEnd()
}

func (x *Empty) UnmarshalProtoJSON(s *json.UnmarshalState) {
    if s.ReadNil() {
        return
    }
    s.ReadObject(func(key string) {
        s.SetErrorf("unexpected key %q in Empty", key)
    })
    *x = Empty{}
}

```

During unmarshaling, the implementation validates that no unexpected fields appear in the JSON object, maintaining strict compatibility with the protobuf specification.

## Dynamic Type Resolution with Any

The **Any** type in [`types/known/anypb/any.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/anypb/any.go) provides the most complex handling among well-known types. It stores arbitrary serialized messages along with a URL that identifies the message type, requiring dynamic type resolution during JSON operations.

### The AnyTypeResolver Contract

Type resolution depends on the **AnyTypeResolver** interface defined in [`types/known/anypb/resolver/resolver.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/anypb/resolver/resolver.go):

```go
type AnyTypeResolver interface {
    FindMessageByURL(url string) (func() Message, error)
}

```

This interface maps type URLs (e.g., `"type.googleapis.com/google.protobuf.Timestamp"`) to factory functions that create empty message instances. The `resolver` package provides helper constructors like `NewFuncAnyTypeResolver` for creating resolvers from simple functions.

### Marshalling Any Types

The `MarshalProtoJSON` method in [`any.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/any.go) writes the `@type` field containing the type URL, then resolves the concrete type using the resolver from `MarshalState`. After creating an instance and unpacking the embedded message, it marshals the inner message using its own `MarshalProtoJSON` method and writes it as the `value` field.

If the resolver is nil or cannot find the type, marshaling returns an error immediately, preventing silent data corruption.

### Unmarshalling Any Types

The `UnmarshalProtoJSON` method first validates that the first field is `@type`, reads the type URL, and resolves the concrete type. For well-known types like `Duration` and `Timestamp`, it expects a specific structure with a `value` field. For other types, it creates a fresh instance and delegates to that type's `UnmarshalProtoJSON` method.

The implementation strictly validates that no extra fields remain after unmarshaling, ensuring round-trip fidelity.

## Practical Code Examples

### Working with Timestamp JSON

```go
import (
    "time"
    "github.com/aperturerobotics/protobuf-go-lite/types/known/timestamppb"
    "github.com/aperturerobotics/protobuf-go-lite/json"
)

func ExampleTimestamp() {
    // Create a Timestamp from time.Now()
    ts := timestamppb.ToTimestamp(time.Now())

    // Marshal to JSON (RFC-3339 format)
    data, _ := json.DefaultMarshalerConfig.Marshal(ts)
    // Output: "2026-02-25T12:34:56Z"

    // Unmarshal back
    var ts2 timestamppb.Timestamp
    _ = json.DefaultUnmarshalerConfig.Unmarshal(data, &ts2)
    
    fmt.Println(ts2.AsTime().Equal(time.Now().Round(time.Second)))
}

```

### Serializing Duration Values

```go
import (
    "github.com/aperturerobotics/protobuf-go-lite/types/known/durationpb"
    "github.com/aperturerobotics/protobuf-go-lite/json"
)

func ExampleDuration() {
    d := &durationpb.Duration{Seconds: 90, Nanos: 0} // 90 seconds

    // Marshal to canonical duration string
    b, _ := json.DefaultMarshalerConfig.Marshal(d)   // → "90s"
    
    var d2 durationpb.Duration
    _ = json.DefaultUnmarshalerConfig.Unmarshal(b, &d2)

    fmt.Println(d2.AsDuration()) // 1m30s
}

```

### Handling Empty Messages

```go
import (
    "github.com/aperturerobotics/protobuf-go-lite/types/known/emptypb"
    "github.com/aperturerobotics/protobuf-go-lite/json"
)

func ExampleEmpty() {
    e := &emptypb.Empty{}
    
    // JSON representation is always empty object
    b, _ := json.DefaultMarshalerConfig.Marshal(e) // → `{}`

    var e2 emptypb.Empty
    _ = json.DefaultUnmarshalerConfig.Unmarshal(b, &e2)
    fmt.Println(e2) // {}
}

```

### Dynamic Any Types with Custom Resolver

```go
import (
    "github.com/aperturerobotics/protobuf-go-lite/types/known/anypb"
    "github.com/aperturerobotics/protobuf-go-lite/types/known/timestamppb"
    "github.com/aperturerobotics/protobuf-go-lite/json"
    "github.com/aperturerobotics/protobuf-go-lite/types/known/anypb/resolver"
)

// Build a resolver that knows only Timestamp
func timestampResolver(url string) (func() protobuf_go_lite.Message, error) {
    if url == "type.googleapis.com/google.protobuf.Timestamp" {
        return func() protobuf_go_lite.Message { return &timestamppb.Timestamp{} }, nil
    }
    return nil, resolver.ErrNotFound
}

func ExampleAny() {
    // Pack a Timestamp into an Any
    ts := timestamppb.Now()
    anyMsg, _ := anypb.New(ts, "type.googleapis.com/google.protobuf.Timestamp")

    // Marshal with custom resolver
    cfg := json.MarshalerConfig{
        AnyTypeResolver: resolver.NewFuncAnyTypeResolver(timestampResolver),
    }
    b, _ := cfg.Marshal(anyMsg)
    // → {"@type":"type.googleapis.com/google.protobuf.Timestamp","value":"2026-02-25T12:34:56Z"}

    // Unmarshal back
    var any2 anypb.Any
    ucfg := json.UnmarshalerConfig{
        AnyTypeResolver: resolver.NewFuncAnyTypeResolver(timestampResolver),
    }
    _ = ucfg.Unmarshal(b, &any2)

    // Extract the Timestamp
    var ts2 timestamppb.Timestamp
    _ = any2.UnmarshalTo(&ts2, "type.googleapis.com/google.protobuf.Timestamp")
    fmt.Println(ts2.AsTime())
}

```

## Summary

- **protobuf-go-lite** implements well-known types as lightweight, reflection-free Go structs in the `types/known/` package.
- **Timestamp** and **Duration** use RFC-3339 and canonical duration string formats via `MarshalState.WriteTime` and `WriteDuration` helpers.
- **Empty** serializes to `{}` and validates that no unexpected fields appear during unmarshaling.
- **Any** requires an **AnyTypeResolver** to map type URLs to message constructors, enabling dynamic serialization of embedded messages without compile-time dependencies.
- All types implement `MarshalProtoJSON` and `UnmarshalProtoJSON` defined in [`json/marshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/json/marshal.go) and [`json/unmarshal.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/json/unmarshal.go), ensuring consistent JSON handling across the library.

## Frequently Asked Questions

### How does protobuf-go-lite differ from google.golang.org/protobuf for well-known types?

**protobuf-go-lite** provides hand-crafted implementations in [`types/known/timestamppb/timestamp.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/timestamppb/timestamp.go) and sibling files that avoid reflection and heavy code generation. While `google.golang.org/protobuf` relies on generated code and the `protoreflect` package, protobuf-go-lite uses direct `MarshalProtoJSON` and `UnmarshalProtoJSON` methods with lightweight state objects, resulting in smaller binary sizes and faster serialization.

### Why does the Any type require a custom resolver in protobuf-go-lite?

The **Any** type stores arbitrary messages identified by a type URL, but the JSON implementation in [`types/known/anypb/any.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/anypb/any.go) cannot know at compile time which concrete message types might be embedded. The **AnyTypeResolver** interface (defined in [`types/known/anypb/resolver/resolver.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/anypb/resolver/resolver.go)) provides a factory function `FindMessageByURL` that returns a constructor for the specific type. Without this resolver, marshaling or unmarshaling Any returns `ErrNoAnyTypeResolver`.

### Can I use standard library JSON marshaling with protobuf-go-lite well-known types?

No, you should use the **protobuf-go-lite JSON runtime** instead of `encoding/json`. The well-known types implement `MarshalProtoJSON(*json.MarshalState)` and `UnmarshalProtoJSON(*json.UnmarshalState)` rather than the standard `json.Marshaler` interface. Use `json.DefaultMarshalerConfig.Marshal()` or create a custom `json.MarshalerConfig` to serialize these types according to the protobuf JSON specification.

### How does protobuf-go-lite handle invalid JSON during unmarshaling?

The **UnmarshalProtoJSON** implementations for well-known types perform strict validation. For example, `Empty` in [`types/known/emptypb/empty.go`](https://github.com/aperturerobotics/protobuf-go-lite/blob/main/types/known/emptypb/empty.go) checks for unexpected fields and returns an error if any appear. `Timestamp` and `Duration` use `ReadTime` and `ReadDuration` helpers that validate RFC-3339 and duration string formats. The `Any` type validates that the first field is `@type` and that no extraneous fields remain after processing.