A declarative OData v2/v4 client for Go.
Documentation - pkg.go.dev - Quick Start - Features - Extensions
Traverse is a Go library for consuming OData v2 and v4 services. It handles all protocol details - pagination, CSRF tokens, ETag concurrency control, delta sync, batch requests, async long-running operations, actions, functions, and schema validation - so you can focus on the data.
Built on relay for HTTP transport. Well-suited for SAP environments (ABAP Gateway / OData v2, S/4HANA / OData v4), Microsoft Graph, and any standards-compliant OData service.
go get github.com/jhonsferg/traverseRequires Go 1.24 or later.
The verb to traverse means to walk through something large, complex, or extended - one step at a time, without needing to hold the whole thing in memory. In computer science, tree traversal and graph traversal describe exactly that: visiting every node of a structure incrementally rather than materialising it all at once.
That is the problem this library solves:
other clients: GET /MaterialSet → load 1 000 000 records into RAM → out of memory
traverse: GET /MaterialSet → visit each record one by one → constant memory
The difference is not what you fetch - it is how you move through it. Traverse treats a remote OData collection the way a graph traversal treats a tree: as a path to walk, not a payload to download.
Three principles follow naturally from the name:
The path matters more than the destination. You do not wait to have all the data before you start working. Each record is actionable the moment it arrives - that is exactly the for result := range client.From("MaterialSet").Stream(ctx) pattern at the core of the library.
Respect for the terrain. A careful traversal does not tear up the ground beneath it. Traverse is deliberately gentle on the services it talks to: rate limiting and circuit breaking are inherited from relay, page size follows the server's own nextLink rhythm, and CSRF tokens are managed transparently without extra round-trips.
The map is not the territory. A tree traversal does not require the full tree in memory - only the current node and a pointer to the next. Traverse does the same with OData: you can walk a million SAP materials without keeping a million structs alive simultaneously.
The name also has an intentional honesty to it: it does not promise to be an OData library or a SAP library. It promises to help you move through large, remote datasets. Today that means OData. Tomorrow it could mean any cursor-based protocol - the name stays valid.
package main
import (
"context"
"fmt"
"log"
"github.com/jhonsferg/traverse"
)
type Product struct {
ID int `json:"ProductID"`
Name string `json:"ProductName"`
Price float64 `json:"UnitPrice"`
}
func main() {
client, err := traverse.New(
traverse.WithBaseURL("https://services.odata.org/V4/Northwind/Northwind.svc/"),
)
if err != nil {
log.Fatal(err)
}
products := traverse.From[Product](client, "Products")
results, err := products.
Filter("UnitPrice lt 20").
OrderBy("ProductName").
Top(10).
List(context.Background())
if err != nil {
log.Fatal(err)
}
for _, p := range results {
fmt.Printf("%s - $%.2f\n", p.Name, p.Price)
}
}| Feature | Description |
|---|---|
| Typed query builder | From[T]() with .Filter(), .Select(), .Expand(), .OrderBy(), .Top(), .Skip() |
| Type-safe filter builder | F("Field").Eq(value), And(), Or(), Not() - no raw strings |
| CRUD operations | .Get(), .List(), .Create(), .Update(), .Delete(), .Upsert() |
| ETag & concurrency | Automatic ETag tracking for optimistic concurrency on updates |
| Entity change tracking | Track and PATCH only modified fields |
| Typed pagination | Paginator[T] with .NextPage() and .Stream() |
| Async operations | Automatic polling for 202 Accepted long-running operations |
| Streaming | Channel-based streaming via json.Decoder - constant memory |
| Batch requests | $batch with transactional changesets; OData 4.01 JSON batch format |
| Delta sync | $deltatoken tracking for incremental data sync |
| Lambda filters | any() / all() on collection navigation properties |
| Deep insert | Create entity graphs in a single request |
| Deep update | PATCH nested entity graphs in a single round-trip |
| BulkUpdate | PATCH /EntitySet?$filter=... for mass updates (OData 4.01) |
| Singletons | First-class singleton access: client.Singleton("me").Page(ctx) |
| Type casting | AsType("Model.Manager") path segments; IsOf() / Cast() filter helpers |
| $expand $levels | Expand("Children", traverse.WithExpandLevels(traverse.LevelsMax)) |
| Geospatial | GeographyPoint, GeometryPoint, GeoDistance, GeoIntersects filter functions |
| $ref link operations | LinkTo() / UnlinkFrom() for managing navigation property references |
| Actions & Functions | ActionBuilder / FunctionBuilder - bound and unbound |
| Schema validation | Client-side field name validation on $filter / $orderby |
| Prefer headers | PreferHandlingStrict, ReturnMinimal, ReturnRepresentation, PreferTrackChanges |
| $schemaversion | WithSchemaVersion("2.0") at client or per-query level |
| Atom/XML responses | OData v2 application/atom+xml streaming parser (auto-detected) |
| OData v2 $inlinecount | $inlinecount=allpages emitted for v2 services; d.__count parsed |
| SAP TLS | WithSAPTLSConfig(cfg) for custom CA bundles and self-signed certs |
| SAP property path | FetchPropertyAs[T] for scalar/complex property fetch by key and path |
| Code generation | traverse-gen generates typed clients from $metadata |
| SAP compatibility | CSRF tokens, X-Requested-With, SAP sap:* metadata attributes |
Full feature documentation: jhonsferg.github.io/traverse
Traverse can parse both EDMX/XML and CSDL JSON (the OData v4.01 JSON format used by Microsoft Graph). The client auto-detects the format by Content-Type when fetching $metadata:
// Auto-detected - no code change needed when the service returns JSON metadata
client, _ := traverse.New(traverse.WithBaseURL("https://api.example.com/odata/v4/"))
meta, err := client.Metadata(ctx)For direct parsing:
import "github.com/jhonsferg/traverse"
// From bytes
meta, err := traverse.ParseCSDLJSON(data)
// From a reader (e.g. http.Response.Body)
meta, err := traverse.ParseCSDLJSONReader(resp.Body)Convert OData metadata to an OpenAPI 3.1 document:
import (
"encoding/json"
"github.com/jhonsferg/traverse"
"github.com/jhonsferg/traverse/ext/openapi"
)
meta, _ := client.Metadata(ctx)
doc, err := openapi.Export(meta,
openapi.WithTitle("My OData API"),
openapi.WithVersion("1.0.0"),
openapi.WithServerURL("https://api.example.com/odata/v4/"),
)
out, _ := json.MarshalIndent(doc, "", " ")
fmt.Println(string(out))go get github.com/jhonsferg/traverse/ext/openapiProperties carry parsed Core and Validation vocabulary annotations:
meta, _ := client.Metadata(ctx)
for _, et := range meta.EntityTypes {
for _, prop := range et.Properties {
core := traverse.ParseCoreVocabulary(prop.Annotations)
val := traverse.ParseValidationVocabulary(prop.Annotations)
fmt.Printf("%s: %s", prop.Name, core.Description)
if val.Pattern != "" {
fmt.Printf(" (pattern: %s)", val.Pattern)
}
if val.Required {
fmt.Print(" [required]")
}
fmt.Println()
}
}Available types: CoreVocabulary (Description, LongDescription, Immutable, Computed, Permissions, …) and ValidationVocabulary (Minimum, Maximum, Pattern, AllowedValues, Required).
traverse-gen generates type-safe Go clients from an OData $metadata endpoint:
go run github.com/jhonsferg/traverse/cmd/traverse-gen \
-metadata https://services.odata.org/V4/Northwind/Northwind.svc/$metadata \
-out ./northwindInteractive terminal UI for exploring OData endpoints, building queries, and inspecting results:
go run github.com/jhonsferg/traverse/cmd/traverse-tuiA local SAP NetWeaver OData v2 simulator for integration testing without a real SAP system:
go run github.com/jhonsferg/traverse/cmd/sap-mockSimulates CSRF token lifecycle, Basic Auth, $metadata responses, entity-set queries, key-predicate lookups, and property-path navigation. Logs all incoming requests with headers, query parameters, and body for inspection.
SAP OData Mock Server
Listen: http://localhost:44300
Auth: enabled (user=sapuser pass=sappass)
| Module | Import path | Description |
|---|---|---|
ext/sap |
github.com/jhonsferg/traverse/ext/sap |
SAP Gateway CSRF, session handling, and Fiori UI annotations |
ext/openapi |
github.com/jhonsferg/traverse/ext/openapi |
OpenAPI 3.1 export from OData metadata |
ext/oauth2 |
github.com/jhonsferg/traverse/ext/oauth2 |
OAuth2 token provider |
ext/prometheus |
github.com/jhonsferg/traverse/ext/prometheus |
Prometheus metrics |
ext/tracing |
github.com/jhonsferg/traverse/ext/tracing |
OpenTelemetry tracing |
ext/graphql |
github.com/jhonsferg/traverse/ext/graphql |
GraphQL-to-OData bridge |
ext/cache |
github.com/jhonsferg/traverse/ext/cache |
HTTP response and metadata caching |
ext/offline |
github.com/jhonsferg/traverse/ext/offline |
Persistent offline store with JSON cache |
ext/dataverse |
github.com/jhonsferg/traverse/ext/dataverse |
Microsoft Dataverse adapter |
ext/azure |
github.com/jhonsferg/traverse/ext/azure |
Azure Event Grid change events |
ext/webhooks |
github.com/jhonsferg/traverse/ext/webhooks |
OData webhook subscriptions |
ext/audit |
github.com/jhonsferg/traverse/ext/audit |
Audit trail middleware |
Extension documentation: jhonsferg.github.io/traverse/extensions
ext/sap includes support for SAP UI annotations parsed from EDMX attributes (sap:label, sap:sortable, sap:filterable, etc.):
import "github.com/jhonsferg/traverse/ext/sap"
ann := sap.ParseSAPUIAnnotation(property.RawAttributes)
fmt.Printf("Label: %s, Filterable: %v\n", ann.Label, ann.Filterable)
// Get all annotated properties from an entity type
props := sap.AnnotatedProperties(entityType, meta)
for _, p := range props {
fmt.Printf("%s → label=%s sortable=%v\n", p.Property.Name, p.Annotation.Label, p.Annotation.Sortable)
}rc := relay.New(relay.WithBearerToken(token))
gc := traverse.NewGraphClient(rc, traverse.GraphConfig{
AccessToken: token,
})
type User struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
}
users, err := traverse.From[User](gc, "users").
Filter("department eq 'Engineering'").
Select("id", "displayName").
List(ctx)The full documentation is at jhonsferg.github.io/traverse:
- Getting Started
- Query Builder Guide
- All Guides
- Code Generation
- API Reference on pkg.go.dev
MIT - see LICENSE.