Migrating from v1 to v2
Version 2.0 finishes the transition that v1.8–v1.9 started: the interim
2-suffixed types and V2 methods become the only API, the deprecated
v1 surface is removed, and models move to backing-store-backed accessors.
This guide maps every v1 construct to its v2 replacement.
If your code compiled against v1.9 without deprecation warnings, you are
already using the v2 shapes — your migration is mostly renames. If you're
coming from an older v1 style (NewServiceNowClient, TableEntry,
response.Result), work through each section below.
v2 is a new major version of the Go module. Once 2.0 is released, install it
with the /v2 module path suffix and update your import paths accordingly.
The examples below use the unsuffixed path, which is correct for the
pre-release previews.
Quick reference
| v1 | v2 |
|---|---|
NewServiceNowClient(cred, url) / NewServiceNowClient2(cred, url) | NewServiceNowServiceClient(opts...) |
credentials.NewUsernamePasswordCredential(u, p) | credentials.NewBasicProvider(u, p) |
credentials.NewTokenCredential(...) | OAuth2 providers: NewROPCProvider, NewClientCredentialsProvider, NewPrivateAuthorizationCodeProvider, … |
client.RequestAdapter (field) | client.GetRequestAdapter() (method) |
client.Now() / client.Now2() | client.Now() |
.Table(name) / .TableV2(name) / .Table2(name) | .Table(name) |
.ById(id) / .ByID2(id) | .ByID(id) |
.Get2(ctx, params), .Post4(ctx, data, params) | .Get(ctx, config), .Post(ctx, data, config) |
import ".../table-api", ".../attachment-api", ".../batch-api" | import ".../tableapi", ".../attachmentapi", ".../batchapi" |
query2 package | query package |
TableRequestBuilder2, TableItemRequestBuilder2GetRequestConfiguration, … | Same name without the 2 |
TableEntry (a map[string]interface{}) | TableRecord (backed model) |
entry.Value("field") → *TableValue | record.Get("field") → (*RecordElement, error) |
entry.Set("field", v) | record.SetValue("field", v) (returns error) |
response.Result (field) | response.GetResult() (method, returns error) |
Client construction
The v1 client took a credential and URL positionally. The v2 client is
option-based, and credentials became Kiota AuthenticationProviders:
cred := credentials.NewUsernamePasswordCredential("username", "password")
client, err := servicenowsdkgo.NewServiceNowClient2(cred, "https://instance.service-now.com")
credAdmin := credentials.NewBasicProvider("{username}", "{password}")
clientPanic, err := servicenowsdkgo.NewServiceNowServiceClient(
servicenowsdkgo.WithAuthenticationProvider(credAdmin),
servicenowsdkgo.WithURL("https://{instance}.service-now.com"),
)
if err != nil {
panic(err)
}
WithURL takes the full base URL; WithInstance takes just the instance
name. See Configuration for the other options
(WithHTTPClient, WithMiddleware, WithRequestAdapter, …).
If you implemented v1's Credential interface (GetAuthentication() (string, error)) yourself, implement Kiota's
authentication.AuthenticationProvider instead — see
Authentication for the built-in providers before
writing your own.
Fluent chain renames
The interim 2/V2 names collapse back to the plain names, now referring to
the new implementations:
response, err := client.Now2().TableV2("incident").ById(sysID).Get(ctx, config)
// Step 3: Configure request
getConfig := &tableapi.TableItemRequestBuilderGetRequestConfiguration{
QueryParameters: &tableapi.TableItemRequestBuilderGetQueryParameters{
// Optional configurations
},
}
// Step 4: Execute request
getResponse, err := client.Now().Table("{TableName}").ByID("{SysID}").Get(context.Background(), getConfig)
if err != nil {
log.Fatal(err)
}
The same applies to every type name: drop the 2
(TableRequestBuilder2 → TableRequestBuilder,
TableItemRequestBuilder2GetRequestConfiguration →
TableItemRequestBuilderGetRequestConfiguration, and the rest). Verb-specific
methods like Get2, Get3, and Post4 are gone; each builder has exactly
one Get/Post/Put/Delete.
Import paths lose their hyphens:
import (
tableapi "github.com/michaeldcanady/servicenow-sdk-go/table-api"
attachmentapi "github.com/michaeldcanady/servicenow-sdk-go/attachment-api"
batchapi "github.com/michaeldcanady/servicenow-sdk-go/batch-api"
)
import (
tableapi "github.com/michaeldcanady/servicenow-sdk-go/v2/tableapi"
attachmentapi "github.com/michaeldcanady/servicenow-sdk-go/v2/attachmentapi"
batchapi "github.com/michaeldcanady/servicenow-sdk-go/v2/batchapi"
)
If you used the experimental query2 package, it's now simply
query.
Models: TableEntry → TableRecord
This is the largest behavioral change. v1's TableEntry was a raw map; v2's
TableRecord is a backed model whose accessors return errors and pointer
values (why?).
Reading:
value := entry.Value("number") // nil if absent
str, err := value.ToString()
element, err := record.Get("number") // error if absent
value, err := element.GetValue() // raw value (vs. GetDisplayValue/GetLink)
str, err := value.GetStringValue() // *string; nil means no value sent
Writing:
entry := tableapi.NewTableEntry()
entry.Set("short_description", "example")
record := tableapi.NewTableRecord()
if err := record.SetValue("short_description", "example"); err != nil {
log.Fatal(err)
}
Responses: Fields → GetResult()
v1 responses exposed results as struct fields; v2 responses are envelopes whose accessors can fail (deserialization is lazy):
response, _ := client.Now().Table("incident").Get(ctx, nil)
for _, entry := range response.Result {
// ...
}
// Get records from the 'incident' table
listGuideResponse, err := client.Now().Table("{TableName}").Get(ctx, nil)
if err != nil {
log.Fatalf("Error: %v", err)
}
results, err := listGuideResponse.GetResult()
if err != nil {
log.Fatal(err)
}
for _, record := range results {
num, err := record.Get("number")
if err != nil {
log.Fatal(err)
}
val, err := num.GetValue()
if err != nil {
log.Fatal(err)
}
strVal, err := val.GetStringValue()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Incident: %s\n", *strVal)
}
Error handling
- v1 methods sometimes returned
nil, nilwhen called on a nil builder; v2 nil-guards always return a sentinel error (snerrors.ErrNilRequestBuilderand friends). Code that relied on silently gettingnilback will now see an error — which is almost certainly what you wanted. - API failures are now typed: match
*core.ServiceNowError(or a specific subtype like*core.NotFoundError) witherrors.As. See Error handling.
Attachments
v1 imported attachment-api; v2 moves to the hyphen-free attachmentapi.
The fluent chain changes too — Attachment() is now on the Now() builder,
and File()/Upload() sit underneath it:
import attachmentapi "github.com/michaeldcanady/servicenow-sdk-go/attachment-api"
file, _ := os.Open("file.txt")
data, _ := io.ReadAll(file)
media := attachmentapi.NewMedia("text/plain", data)
config := &attachmentapi.AttachmentFileRequestBuilderPostRequestConfiguration{
QueryParameters: &attachmentapi.AttachmentFileRequestBuilderPostQueryParameters{
TableSysID: &sysID,
TableName: &tableName,
FileName: &fileName,
},
}
response, err := client.Attachment().File().Post(ctx, media, config)
file, err := os.Open("path/to/file.txt")
if err != nil {
log.Fatal(err)
}
defer func() { _ = file.Close() }()
// Upload attachment for an incident
guideTableName := "{TableName}"
guideTableSysId := "{SysID}"
guideFileName := "file.txt"
params := &attachmentapi.AttachmentFileRequestBuilderPostQueryParameters{
TableName: &guideTableName,
TableSysID: &guideTableSysId,
FileName: &guideFileName,
}
config := &attachmentapi.AttachmentFileRequestBuilderPostRequestConfiguration{
QueryParameters: params,
}
// Assuming 'file' can be used as media content
createGuideResponse, err := client.Now().Attachment().File().Post(ctx, nil, config) // Placeholder
if err != nil {
log.Fatal(err)
}
result, err := createGuideResponse.GetResult()
if err != nil {
log.Fatal(err)
}
id, err := result.GetSysID()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created attachment with sys_id: %s\n", *id)
sysIdToDownload := "{SysID}"
config2 := &attachmentapi.AttachmentItemFileRequestBuilderGetRequestConfiguration{}
downloadFile, err := client.Now().Attachment().ByID(sysIdToDownload).File().Get(ctx, config2)
if err != nil {
log.Fatal(err)
}
content, _ := downloadFile.GetContent()
// content is a []byte containing the file data
See Attachments for the full upload and download guides.
Pagination
v1 had no built-in pagination helper — callers used Offset/Limit query
parameters and looped manually. v2 introduces
core.PageIterator, which follows Link headers across
pages automatically:
offset := int32(0)
limit := int32(100)
for {
params := &tableapi.TableRequestBuilderGetQueryParameters{
Offset: &offset,
Limit: &limit,
}
response, _ := client.Now().Table("incident").Get(ctx,
&tableapi.TableRequestBuilderGetRequestConfiguration{QueryParameters: params})
results, _ := response.GetResult()
if len(results) == 0 {
break
}
for _, record := range results {
// process record
}
offset += limit
}
// Fetch the next page of results manually
nextPage, err := iterator.Next(ctx)
if err != nil {
log.Fatal(err)
}
// Process the items on the next page
results := nextPage.Result
for _, item := range results {
fmt.Println(item)
}
// Iterate item by item using NextItem
for iterator.HasNext() {
item, err := iterator.NextItem(ctx)
if err != nil {
break
}
fmt.Println(item)
}
The iterator also works for attachments and other collection endpoints — see Pagination for the full API including reverse iteration and state management.
Queries
The query2 package is now simply query. The fluent builder API is
unchanged — build conditions, chain them, and call .String():
import "github.com/michaeldcanady/servicenow-sdk-go/query2"
q := query2.String("short_description").Contains("System").
And(query2.Number("priority").Is(1)).String()
params := &tableapi.TableRequestBuilderGetQueryParameters{Query: &q}
response, err := client.Now().Table("incident").Get(ctx,
&tableapi.TableRequestBuilderGetRequestConfiguration{QueryParameters: params})
q := query.String("short_description").Contains("System").
And(query.Number("priority").Is(1)).String()
fmt.Println(q) // Output: short_descriptionLIKESystem^priority=1
// Build the query
q2 := query.Boolean("active").Is(true).
And(query.String("priority").
IsOneOf("1", "2")).String()
params := &tableapi.TableRequestBuilderGetQueryParameters{
Query: &q2,
}
config := &tableapi.TableRequestBuilderGetRequestConfiguration{
QueryParameters: params,
}
response, err := client.Now().Table("{TableName}").Get(ctx, config)
if err != nil {
log.Fatal(err)
}
See Query builder for the full condition API.
Authentication
v1's Credential interface (GetAuthentication() (string, error)) is
replaced by Kiota's authentication.AuthenticationProvider. The built-in
providers follow the same construction pattern — create a provider, pass it
to the client:
cred := credentials.NewUsernamePasswordCredential("user", "pass")
client, _ := servicenowsdkgo.NewServiceNowClient2(cred, url)
cred := credentials.NewBasicProvider("{username}", "{password}")
client, err := servicenowsdkgo.NewServiceNowServiceClient(
servicenowsdkgo.WithAuthenticationProvider(cred),
servicenowsdkgo.WithURL("https://{instance}.service-now.com"),
)
if err != nil {
log.Fatal(err)
}
OAuth2 providers use the same shape — NewROPCProvider, NewClientCredentialsProvider,
NewPrivateAuthorizationCodeProvider, and NewJWTProvider all satisfy
AuthenticationProvider:
cred, err := credentials.NewROPCProvider(
"{clientID}",
"{clientSecret}",
"{username}",
"{password}",
credentials.WithInstance("{instance}"),
)
if err != nil {
log.Fatal(err)
}
client, err := servicenowsdkgo.NewServiceNowServiceClient(
servicenowsdkgo.WithAuthenticationProvider(cred),
servicenowsdkgo.WithInstance("{instance}"),
)
if err != nil {
log.Fatal(err)
}
// Client is now authenticated and ready to use
_ = client
If you implemented v1's Credential interface yourself, implement Kiota's
authentication.AuthenticationProvider instead — see
Authentication for the full provider reference.
Migration checklist
- Update the module and fix import paths (
table-api→tableapi, etc.). - Replace client construction with
NewServiceNowServiceClient+ options. - Replace credentials with the matching provider
(
NewBasicProvideror an OAuth2 provider). - Rename chain methods (
Now2→Now,TableV2→Table,ById→ByID) and strip2suffixes from types. - Convert
TableEntrycode toTableRecordaccessors — this is where the compiler will do most of the finding for you. - Replace
.Resultfield access withGetResult()and handle the error. - Re-check error handling: add
errors.Is/errors.Aswhere you previously compared strings or ignorednil, nil.
If something doesn't map cleanly, open an issue — gaps in this guide are release blockers for us.