Skip to main content
Version: main

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.

note

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

v1v2
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 packagequery package
TableRequestBuilder2, TableItemRequestBuilder2GetRequestConfiguration, …Same name without the 2
TableEntry (a map[string]interface{})TableRecord (backed model)
entry.Value("field")*TableValuerecord.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:

v1
cred := credentials.NewUsernamePasswordCredential("username", "password")
client, err := servicenowsdkgo.NewServiceNowClient2(cred, "https://instance.service-now.com")
v2
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:

v1
response, err := client.Now2().TableV2("incident").ById(sysID).Get(ctx, config)
v2
// 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 (TableRequestBuilder2TableRequestBuilder, TableItemRequestBuilder2GetRequestConfigurationTableItemRequestBuilderGetRequestConfiguration, 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:

v1
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"
)
v2
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: TableEntryTableRecord

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:

v1
value := entry.Value("number") // nil if absent
str, err := value.ToString()
v2
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:

v1
entry := tableapi.NewTableEntry()
entry.Set("short_description", "example")
v2
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):

v1
response, _ := client.Now().Table("incident").Get(ctx, nil)
for _, entry := range response.Result {
// ...
}
v2
// 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, nil when called on a nil builder; v2 nil-guards always return a sentinel error (snerrors.ErrNilRequestBuilder and friends). Code that relied on silently getting nil back 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) with errors.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:

v1
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)
v2
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:

v1
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
}
v2 — iterate all pages
// 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)
}
v2 — item by 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():

v1
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})
v2
q := query.String("short_description").Contains("System").
And(query.Number("priority").Is(1)).String()

fmt.Println(q) // Output: short_descriptionLIKESystem^priority=1
v2 — pass to table API
// 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:

v1
cred := credentials.NewUsernamePasswordCredential("user", "pass")
client, _ := servicenowsdkgo.NewServiceNowClient2(cred, url)
v2
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:

v2 — ROPC
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

  1. Update the module and fix import paths (table-apitableapi, etc.).
  2. Replace client construction with NewServiceNowServiceClient + options.
  3. Replace credentials with the matching provider (NewBasicProvider or an OAuth2 provider).
  4. Rename chain methods (Now2Now, TableV2Table, ByIdByID) and strip 2 suffixes from types.
  5. Convert TableEntry code to TableRecord accessors — this is where the compiler will do most of the finding for you.
  6. Replace .Result field access with GetResult() and handle the error.
  7. Re-check error handling: add errors.Is/errors.As where you previously compared strings or ignored nil, nil.

If something doesn't map cleanly, open an issue — gaps in this guide are release blockers for us.

Was this page helpful?