Skip to content

feat!: generate Go SDK from OpenAPI spec#524

Open
gjtorikian wants to merge 27 commits intonext-majorfrom
oagen
Open

feat!: generate Go SDK from OpenAPI spec#524
gjtorikian wants to merge 27 commits intonext-majorfrom
oagen

Conversation

@gjtorikian
Copy link
Copy Markdown
Contributor

Description

This branch introduces the v7 Go SDK surface behind a single root workos package and shared workos.Client.

It also:

  • regenerates the API surface and models from the spec
  • removes the legacy pkg/* service packages in favor of client.Service() accessors
  • updates list, error, and webhook patterns to match the new client shape
  • adds docs/V7_MIGRATION_GUIDE.md to document consumer migration

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

gjtorikian and others added 16 commits April 5, 2026 23:27
Complete rewrite of the Go SDK generated by the oagen Go emitter.
Flat workos package with idiomatic Go patterns:

- NewClient(apiKey, ...ClientOption) functional options
- context.Context on all methods
- Typed string enum constants
- Iterator[T] for auto-pagination
- SDK-native error types
- Exponential backoff retry with jitter
- httptest-based generated tests

This is a breaking change from the v1 SDK architecture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 8 AuthenticateWith* wrappers on UserManagement
- Add CreateOAuthApplication and CreateM2MApplication on Connect
- Fix test imports to use correct module path (v6)
- Fix params struct names to use service-prefixed convention
- Deduplicate test functions for merged mount groups
- Preserve original go.mod (not overwritten by emitter)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements Passwordless, Vault KV/crypto, webhook verification,
Actions helper, session seal/unseal, PKCE, AuthKit/SSO URL builders,
JWKS helper, and public client factory (H01-H19).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gjtorikian gjtorikian requested review from a team as code owners April 7, 2026 15:57
@gjtorikian gjtorikian requested review from atainter and removed request for a team April 7, 2026 15:57
@gjtorikian gjtorikian changed the title feat!: rewrite Go SDK around unified root client feat!: generate Go SDK from OpenAPI spec Apr 7, 2026
gjtorikian and others added 6 commits April 8, 2026 11:51
Regenerated from spec changes: extracted DirectoryUser/EventContext
schemas, SlimRole refs, limit type number->integer, RoleList ref.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@swaroopAkkineniWorkos
Copy link
Copy Markdown
Contributor

@greptile review

Comment thread passwordless.go Outdated
)

// passwordlessService handles Passwordless session operations.
type passwordlessService struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passwordless.go:16 and vault_kv.go:16 both initialize the client in the separate file, vs in workos.go like the other clients

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 — this is an inconsistency. All other services are initialized once in NewClient() and stored as fields on Client. Passwordless() and Vault() create a new allocation on every call, which is both inconsistent and wasteful.

Fix: add passwordless *passwordlessService and vault *vaultService fields to the Client struct, initialize them in NewClient() alongside the others, and have the accessors return the cached instance:

// In NewClient():
c.passwordless = &passwordlessService{client: c}
c.vault = &vaultService{client: c}

// Accessor:
func (c *Client) Passwordless() *passwordlessService {
    return c.passwordless
}

Comment thread user_management.go Outdated

// UserManagementCreateAuthenticateParams contains the parameters for CreateAuthenticate.
type UserManagementCreateAuthenticateParams struct {
Body interface{} `json:"-"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason for the untyped blog here for the entire body?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — Body interface{} here loses all type safety. Consumers can pass literally anything (a string, an int, a struct with wrong fields) and it'll only fail at runtime.

Since there are already dedicated typed methods for each grant type (AuthenticateWithPassword, AuthenticateWithCode, etc.), this generic CreateAuthenticate with an untyped body may not need to be part of the public API at all. If it does need to stay (e.g., for forward compatibility with new grant types), consider a sealed interface pattern:

type AuthenticateBody interface {
    authenticateBody() // unexported method = sealed to this package
}

The same issue applies to ConnectCreateApplicationParams.Body in connect.go:56.

Comment thread authorization.go Outdated
Comment on lines +44 to +49
ParentResourceID *string `url:"parent_resource_id,omitempty" json:"-"`
// ParentResourceTypeSlug is the slug of the parent resource type. Must be provided together with `parent_resource_external_id`.
ParentResourceTypeSlug *string `url:"parent_resource_type_slug,omitempty" json:"-"`
// ParentResourceExternalID is the application-specific external identifier of the parent resource. Must be provided together with `parent_resource_type_slug`.
ParentResourceExternalID *string `url:"parent_resource_external_id,omitempty" json:"-"`
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be cool if we could put types like (ParentResourceTypeSlug + ParentResourceExternalID) into it's own object so they are grouped together by default. This comes up a lot across the sdk

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fair suggestion. The pattern of ParentResourceTypeSlug + ParentResourceExternalID appearing together across many param structs (here, AuthorizationListOrganizationMembershipResourcesParams, AuthorizationListResourcesParams, CreateAuthorizationResource, UpdateAuthorizationResource, etc.) is a good candidate for a shared embedded struct:

// ParentResourceRef identifies a parent resource by external ID and type.
type ParentResourceRef struct {
    ParentResourceID         *string `url:"parent_resource_id,omitempty" json:"parent_resource_id,omitempty"`
    ParentResourceTypeSlug   *string `url:"parent_resource_type_slug,omitempty" json:"parent_resource_type_slug,omitempty"`
    ParentResourceExternalID *string `url:"parent_resource_external_id,omitempty" json:"parent_resource_external_id,omitempty"`
}

// Similarly for resource identification:
type ResourceRef struct {
    ResourceID         *string `json:"resource_id,omitempty"`
    ResourceExternalID *string `json:"resource_external_id,omitempty"`
    ResourceTypeSlug   *string `json:"resource_type_slug,omitempty"`
}

Then embed them:

type AuthorizationCheckParams struct {
    PermissionSlug string `json:"permission_slug"`
    ResourceRef           // embedded
}

This would be a codegen improvement — teaching oagen to detect these co-occurring field groups and extract shared types.

@devin-ai-integration
Copy link
Copy Markdown
Contributor

Go Best Practices Review

I reviewed the full PR diff. Overall this is a well-structured SDK rewrite — the flat workos package, functional options pattern, generic Iterator[T], typed errors with Unwrap(), and auto-retry with exponential backoff are all idiomatic Go. Here are the issues I found, ordered by severity:


🔴 Critical: Module path says v6 but version is v7

go.mod:3 declares module github.com/workos/workos-go/v6 but version.go:7 says Version = "v7.0.0". Per Go module versioning rules, a v7 major version must have /v7 in the module path. Consumers won't be able to go get github.com/workos/workos-go/v7 until this is fixed. All internal import paths in test files also reference /v6.


🟠 High: Untyped Body interface{} fields defeat type safety

user_management.go:30 and connect.go:56 both have:

type UserManagementCreateAuthenticateParams struct {
    Body interface{} `json:"-"`
}

This forces consumers to pass an arbitrary interface{}, losing all compile-time type checking. If the body must support multiple shapes (e.g., different grant types), consider:

  • A union interface: type AuthenticateBody interface { authenticateBody() } with concrete types implementing it
  • Or separate typed methods per grant type (which you already have — AuthenticateWithPassword, AuthenticateWithCode, etc.), making CreateAuthenticate potentially unnecessary as a public API

🟠 High: map[string]interface{} request bodies in authenticate methods

Throughout user_management.go (lines 62-84, 103-121, 146-167, 188-210, etc.), sso.go:184-195, and connect.go:90-115, request bodies are built as map[string]interface{} with manual nil checks:

body := map[string]interface{}{
    "grant_type": "password",
    "email":      params.Email,
    "password":   params.Password,
}
if params.InvitationToken != nil {
    body["invitation_token"] = *params.InvitationToken
}

This is fragile and un-idiomatic Go. Prefer typed structs with json:"...,omitempty" tags — it's more maintainable, avoids runtime key typos, and lets encoding/json handle nil/omit logic:

type authenticateWithPasswordBody struct {
    GrantType       string  `json:"grant_type"`
    Email           string  `json:"email"`
    Password        string  `json:"password"`
    ClientID        string  `json:"client_id,omitempty"`
    ClientSecret    string  `json:"client_secret,omitempty"`
    InvitationToken *string `json:"invitation_token,omitempty"`
    // ...
}

🟡 Medium: Unexported service types returned from exported methods

All service accessors return unexported types:

func (c *Client) SSO() *ssoService { ... }
func (c *Client) UserManagement() *userManagementService { ... }

This works but prevents consumers from declaring typed variables, writing helper functions, or building abstractions:

// Consumer cannot write:
func setupSSO(sso *workos.ssoService) { ... } // compile error — unexported

// They must use:
func setupSSO(client *workos.Client) {
    client.SSO().ListConnections(...)
}

The standard Go approach is to either export the concrete type, or define an exported interface per service. AWS SDK, Stripe SDK, etc. all export their service types.


🟡 Medium: *[]string and *map[string]string pointer-to-reference types

In models.go (lines 64, 66, 84, 96, 98, 368, 388, 515, 537, 674), fields use *[]string, *[]*RedirectURIInput, and *map[string]string. In Go, slices and maps are already reference types — a nil slice with omitempty is already omitted by encoding/json. Pointer-to-slice/map adds unnecessary indirection and makes the API harder to use:

// Awkward for consumers:
scopes := []string{"read", "write"}
params := &CreateOAuthApplicationParams{
    Scopes: &scopes,  // need address-of a local
}

// Better:
params := &CreateOAuthApplicationParams{
    Scopes: []string{"read", "write"},  // direct
}

Exception: *map[string]string with omitempty is needed only if you must distinguish "field absent" from "empty map {}" in the JSON. If that distinction isn't required by the API, plain map[string]string suffices.


🟡 Medium: ConstructEvent / ConstructAction / SignResponse return untyped maps

WebhookVerifier.ConstructEvent() and ActionsHelper.ConstructAction() return map[string]interface{}, forcing consumers into type assertions. SignResponse also returns map[string]interface{} with keys "payload" and "sig". These should return typed structs for a better developer experience:

type WebhookEvent struct { /* typed fields */ }
type ActionSignedResponse struct {
    Payload string `json:"payload"`
    Sig     string `json:"sig"`
}

🟢 Low: Grammar in generated doc comments

Several generated comments have "an user" instead of "a user" (e.g., models.go:5 "represents an user object", models.go:19 "represents an user consent option", models.go:31). The article before "user" should be "a" not "an". This is a codegen template fix.


🟢 Low: GetAuthorizationURL makes an HTTP call instead of returning a URL

userManagementService.GetAuthorizationURL (user_management.go:409) constructs query params and then fires a GET request to /user_management/authorize. But authorization URL methods typically build and return a URL string for the consumer to redirect to — not make an actual HTTP request. Same pattern in ssoService.GetAuthorizationURL. Worth verifying this is intentional vs. a codegen artifact.


Summary

Issue Severity Generated vs Hand-written
Module path v6/v7 mismatch 🔴 Critical Hand-written (go.mod)
Untyped Body interface{} 🟠 High Generated
map[string]interface{} bodies 🟠 High Hand-written
Inconsistent service init (see thread below) 🟡 Medium Hand-written
Unexported service types 🟡 Medium Generated
*[]string / *map types 🟡 Medium Generated
Untyped return maps 🟡 Medium Hand-written
"an user" grammar 🟢 Low Generated
GetAuthorizationURL behavior 🟢 Low Generated/Hand-written

Most of the generated code issues point to improvements needed in the oagen code generator templates. The hand-written code issues (authenticate bodies, service init consistency) can be addressed directly.

gjtorikian and others added 5 commits April 15, 2026 14:39
P0: URL-encode all path parameters with url.PathEscape to prevent
malformed URLs from IDs containing special characters. URL-encode
returnTo in GetLogoutURL with url.QueryEscape.

P1: Deduplicate HMAC signing logic in actions_helper.go by reusing
exported functions from webhook_verification.go. Replace
map[string]interface{} with typed structs (User, AuthenticateResponseImpersonator,
JWTClaims) in session helpers for type safety. Extract shared
pkceAndState helper in PublicClient and eliminate throwaway Client
allocations by storing a minimal Client at construction time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… support

Resolves #198 by parsing both code/message and error/error_description JSON
formats, exposing ErrorCode and ErrorDescription on APIError, and returning
typed errors (EmailVerificationRequiredError, MFAEnrollmentError, etc.) that
callers can match with errors.As.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants