Migration Guide: v3.7.4 → v4.1.x
This guide covers all breaking changes and required code updates when migrating an Asset Link implementation from SDK v3.7.4 (module github.com/industrial-asset-hub/asset-link-sdk/v3) to v4.1.x (module github.com/industrial-asset-hub/asset-link-sdk/v4).
Table of Contents
- Module path change (v3 → v4)
- Identifiers API replaced by DeviceInfo API
- Asset data model changes (base schema v0.12.0 → v1.20.0)
- Builder API changes
- Metadata struct: new optional fields
- Model helper method signature changes (errors are now returned)
- Removed types and functions
- New types and functions
- Registry interface constant rename
- New generated package: conn_suite_device_info
- Dependency updates
1. Module path change (v3 → v4)
All import paths must be updated from v3 to v4.
Before:
import "github.com/industrial-asset-hub/asset-link-sdk/v3/assetlink"
import "github.com/industrial-asset-hub/asset-link-sdk/v3/model"
import "github.com/industrial-asset-hub/asset-link-sdk/v3/config"
import "github.com/industrial-asset-hub/asset-link-sdk/v3/metadata"
import generated "github.com/industrial-asset-hub/asset-link-sdk/v3/generated/iah-discovery"
After:
import "github.com/industrial-asset-hub/asset-link-sdk/v4/assetlink"
import "github.com/industrial-asset-hub/asset-link-sdk/v4/model"
import "github.com/industrial-asset-hub/asset-link-sdk/v4/config"
import "github.com/industrial-asset-hub/asset-link-sdk/v4/metadata"
import generated "github.com/industrial-asset-hub/asset-link-sdk/v4/generated/iah-discovery"
Update go.mod:
module your-asset-link
go 1.25.13
require (
github.com/industrial-asset-hub/asset-link-sdk/v4 <latest-version>
)
Run go mod tidy afterwards.
2. Identifiers API replaced by DeviceInfo API
The Identifiers interface and the gRPC IdentifiersApi have been removed and replaced by the new DeviceInfo API (conn_suite_device_info).
Interface change
Before (internal/features/features.go):
// config.IdentifiersRequest is a helper wrapping GetIdentifiersRequest
type Identifiers interface {
GetIdentifiers(identifiersRequest config.IdentifiersRequest) ([]*generated.DeviceIdentifier, error)
}
After:
import deviceinfo "github.com/industrial-asset-hub/asset-link-sdk/v4/generated/conn_suite_device_info"
type DeviceInfo interface {
GetPropertyValues(request *deviceinfo.GetPropertyValuesRequest) (*deviceinfo.GetPropertyValuesResponse, error)
GetSupportedProperties(request *deviceinfo.GetSupportedPropertiesRequest) (*deviceinfo.GetSupportedPropertiesResponse, error)
}
Implementation change
Before:
func (m *MyAssetLink) GetIdentifiers(req config.IdentifiersRequest) ([]*generated.DeviceIdentifier, error) {
// build and return identifiers
return identifiers, nil
}
After:
The GetPropertyValues method receives a device target in the request and must return the asset’s properties as a flat list of key/value pairs. Use DeviceInfo.ConvertToPropertyValueResults() to convert a populated DeviceInfo into the expected response format:
import (
deviceinfo "github.com/industrial-asset-hub/asset-link-sdk/v4/generated/conn_suite_device_info"
generated "github.com/industrial-asset-hub/asset-link-sdk/v4/generated/iah-discovery"
)
func (m *MyAssetLink) GetPropertyValues(req *deviceinfo.GetPropertyValuesRequest) (*deviceinfo.GetPropertyValuesResponse, error) {
// 1. Extract connection parameters from the request target
device := req.GetDevice()
if device == nil {
return nil, status.Errorf(codes.InvalidArgument, "missing device target")
}
paramJSON := device.GetConnectionParameterSet().GetParameterJson()
credentials := device.GetConnectionParameterSet().GetCredentials()
// 2. Retrieve device details using paramJSON / credentials (protocol-specific)
deviceDetails, err := m.retrieveDevice(paramJSON, credentials)
if err != nil {
return nil, status.Errorf(codes.Unavailable, "could not reach device: %v", err)
}
// 3. Build the DeviceInfo model
deviceInfo, err := buildDeviceInfo(deviceDetails)
if err != nil {
return nil, err
}
// 4. Convert to property value results using the SDK helper
results, err := deviceInfo.ConvertToPropertyValueResults()
if err != nil {
return nil, err
}
return &deviceinfo.GetPropertyValuesResponse{PropertyResults: results}, nil
}
func (m *MyAssetLink) GetSupportedProperties(_ *deviceinfo.GetSupportedPropertiesRequest) (*deviceinfo.GetSupportedPropertiesResponse, error) {
return &deviceinfo.GetSupportedPropertiesResponse{
Properties: []*deviceinfo.SupportedProperty{
{Key: "name", Type: &deviceinfo.SupportedProperty_Datatype{Datatype: generated.VariantType_VT_STRING}},
{Key: "functional_object_type", Type: &deviceinfo.SupportedProperty_Datatype{Datatype: generated.VariantType_VT_STRING}},
{Key: "functional_object_schema_url", Type: &deviceinfo.SupportedProperty_Datatype{Datatype: generated.VariantType_VT_STRING}},
{Key: "asset_identifiers", Type: &deviceinfo.SupportedProperty_Datatype{Datatype: generated.VariantType_VT_ARRAY}},
{Key: "connection_points", Type: &deviceinfo.SupportedProperty_Datatype{Datatype: generated.VariantType_VT_ARRAY}},
{Key: "software_components", Type: &deviceinfo.SupportedProperty_Datatype{Datatype: generated.VariantType_VT_ARRAY}},
{Key: "product_instance_information", Type: &deviceinfo.SupportedProperty_Datatype{Datatype: generated.VariantType_VT_STRUCT}},
},
}, nil
}
ConvertToPropertyValueResults() internally calls deviceInfo.ConvertToJson() (which produces a map[string]interface{}) and then wraps each key/value pair into a PropertyValueResult. You do not need to call ConvertToJson manually.
See cdm-al-reference/reference/reference.go for a complete working example.
Removed config helper
config.IdentifiersRequest, config.NewIdentifiersRequestFromGetIdentifiersReq, and config/identifiersrequest_impl.go have been deleted. The new config.DeviceInfoRequest interface (config/deviceinforequest.go) exists but is not directly used by the DeviceInfo interface — requests arrive as typed protobuf messages from the conn_suite_device_info package.
registry.json change
Replace "siemens.common.identifiers.v1" with "siemens.connectivitysuite.deviceinfo.v1" in your registry.json. Keep the other interface entries intact:
Before:
{
"app_instance_id": "cdm-device-class-driver-com.example.al.mylink",
"app_types": [
"siemens.connectivitysuite.drvinfo.v1",
"siemens.industrialassethub.discover.v1",
"siemens.common.identifiers.v1"
],
"driver_schema_uris": ["com.example.al.mylink"]
}
After:
{
"app_instance_id": "cdm-device-class-driver-com.example.al.mylink",
"app_types": [
"siemens.connectivitysuite.drvinfo.v1",
"siemens.industrialassethub.discover.v1",
"siemens.connectivitysuite.deviceinfo.v1"
],
"driver_schema_uris": ["com.example.al.mylink"]
}
If your Asset Link does not implement the DeviceInfo interface, simply omit "siemens.connectivitysuite.deviceinfo.v1" from app_types.
3. Asset data model changes (base schema v0.12.0 → v1.20.0)
The base schema has been updated from v0.12.0 to v1.20.0. This is a significant restructuring of the asset model.
3.1 Schema URL
| Before | After |
|---|---|
https://schema.industrial-assets.io/base/v0.12.0 | https://industrial-assets.io/schemas/iah/base-schema/released/v1 |
The canonical schema URL is now exposed as the constant model.FunctionalObjectSchemaUrl:
const FunctionalObjectSchemaUrl = "https://industrial-assets.io/schemas/iah/base-schema/released/v1/iah-base.json"
3.2 DeviceInfo struct
| Removed field | Replacement |
|---|---|
Type string (@type) | FunctionalObjectType any (functional_object_type) |
Context *AssetContext (@context) | FunctionalObjectSchemaUrl any (functional_object_schema_url) |
MacIdentifiers []MacIdentifier | AssetIdentifiers []any (use AddMacIdentifier) |
3.3 Asset struct (embedded in DeviceInfo)
| Removed field | Replacement / note |
|---|---|
Id string | Removed (no longer required) |
ManagementState ManagementState | Removed — no longer part of the schema |
ReachabilityState *ReachabilityState | Removed |
ProductInstanceIdentifier *ProductSerialIdentifier | ProductInstanceInformation interface{} |
CustomUiProperties []CustomProperty | Removed |
FunctionalParts []interface{} | Removed |
LastModifiedTimestamp *time.Time | Removed |
Zone *string | Removed |
Responsible *string | Removed |
OtherStates []State | Removed |
AssetIdentifiers []interface{} (omitempty) | AssetIdentifiers []interface{} (now required, not omitempty) |
| — | AssetRelations []AssetRelation (new) |
| — | OperatingMode interface{} (new) |
3.4 GatewayInfo / NewGateway
GatewayInfo and NewGateway() have been removed. Use the new FunctionalObjectType constants directly when needed:
const GatewayFunctionalObjectTypeGateway GatewayFunctionalObjectType = "Gateway"
3.5 NewDevice now returns an error
Before:
deviceInfo := model.NewDevice("EthernetDevice", assetName)
After:
deviceInfo, err := model.NewDevice(string(model.DeviceFunctionalObjectTypeDevice), assetName)
if err != nil {
// handle validation error
}
NewDevice now validates that the functionalObjectType is non-empty and belongs to the allowed set:
model.AssetFunctionalObjectTypeAsset("Asset")model.DeviceFunctionalObjectTypeDevice("Device")model.GatewayFunctionalObjectTypeGateway("Gateway")model.SoftwareArtifactFunctionalObjectTypeSoftwareArtifact("SoftwareArtifact")
4. Builder API changes
Identifiers(...) renamed to DeviceInfo(...)
Before:
alImpl := assetlink.New(metadata.Metadata{...}).
Discovery(myImpl).
Identifiers(myImpl).
Build()
After:
alImpl := assetlink.New(metadata.Metadata{...}).
Discovery(myImpl).
DeviceInfo(myImpl). // renamed
Build()
If your Asset Link does not implement the DeviceInfo interface, simply omit the .DeviceInfo(...) call.
5. Metadata struct: new optional fields
Three new optional fields were added to metadata.Metadata:
Before:
metadata.Metadata{
AlId: "com.example.al.mylink",
AlName: "My Asset Link",
Vendor: "My Company",
Version: metadata.Version{Version: version, Commit: commit, Date: date},
}
After:
metadata.Metadata{
AlId: "com.example.al.mylink",
AlName: "My Asset Link",
Vendor: "My Company",
Version: metadata.Version{Version: version, Commit: commit, Date: date},
Description: "Short description of what this Asset Link does.", // new
DocUrl: "https://example.com/docs", // new
FeedbackUrl: "https://example.com/feedback", // new
}
All three new fields are optional strings and can be left empty.
6. Model helper method signature changes (errors are now returned)
All model helper methods that previously silently ignored invalid inputs now return an error. Callers must handle the returned errors.
AddNameplate
Before:
// parameter: manufacturerProductDesignation
deviceInfo.AddNameplate(vendorName, productUri, orderNumber, productName, hardwareVersion, serialNumber)
After:
// parameters renamed/extended:
// manufacturerProductDesignation -> productFamily
// new optional argument: productType
err := deviceInfo.AddNameplate(vendorName, productUri, orderNumber, productFamily, hardwareVersion, serialNumber, productType)
if err != nil {
// handle error
}
Note the parameter changes:
manufacturerProductDesignationwas renamed toproductFamily.productTypewas added as a new argument and maps toproduct_instance_information.manufacturer_product.product_type.productTypeis optional for validation: empty values do not trigger an error and the field is omitted from output.
AddDescription
Before:
deviceInfo.AddDescription("My Device")
After:
err := deviceInfo.AddDescription("My Device")
AddCapabilities
Before:
deviceInfo.AddCapabilities("firmware_update", false)
After:
err := deviceInfo.AddCapabilities("firmware_update", false)
AddNic
Before:
nicId := deviceInfo.AddNic("eth0", "00:16:3e:01:02:03")
After:
nicId, err := deviceInfo.AddNic("eth0", "00:16:3e:01:02:03")
if err != nil {
// handle validation error (e.g., invalid MAC format)
}
AddNic now validates the MAC address against MacAddressPattern (^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$).
A new variant AddNicWithoutMacIdentifier is available when you want to add a NIC without automatically appending a MacIdentifier to AssetIdentifiers.
AddIPv4
Before:
id := deviceInfo.AddIPv4(nicId, "192.168.0.10", "255.255.255.0", "")
After:
// err is nil here because IP and mask are non-empty (error fires only when ALL fields are empty)
id, err := deviceInfo.AddIPv4(nicId, "192.168.0.10", "255.255.255.0", "")
if err != nil {
// only reached when ipv4Address, networkMask, AND routerAddress are all empty
}
Important behavior change: The error is returned only when all three address arguments are empty strings. If at least one field is non-empty, the call succeeds; invalid individual fields log a warning and are skipped. In v3 an empty routerAddress was silently ignored; in v4 the same call behaves identically.
AddIPv6
Before:
id := deviceInfo.AddIPv6(nicId, ipv6Addr, prefix, router)
After:
id, err := deviceInfo.AddIPv6(nicId, ipv6Addr, prefix, router)
The same OR-logic applies: error only when all three fields are empty.
AddSoftware renamed to AddSoftwareArtifactComponent
Before:
deviceInfo.AddSoftware("Firmware", "1.0.0", true)
After:
err := deviceInfo.AddSoftwareArtifactComponent("Firmware", "1.0.0", true)
A new AddRunningSoftwareComponent method is also available for running software instances identified by a runningSoftwareId.
7. Removed types and functions
| Removed symbol | Notes |
|---|---|
model.GatewayInfo | Use DeviceInfo with GatewayFunctionalObjectTypeGateway |
model.NewGateway() | Removed with GatewayInfo |
model.AssetContext | JSON-LD @context is no longer part of the schema |
model.ArtifactChecksum | Removed from base schema |
model.AssetIdentifier / AssetIdentifierAssetIdentifierType | Removed; use specific identifier types |
model.ProductSerialIdentifier | Replaced by ProductInstanceInformation |
model.ManagementState / ManagementStateValues* | Removed from schema |
model.ReachabilityState / ReachabilityStateValues* | Removed from schema |
model.State | Removed from schema |
model.CustomProperty | Removed from schema |
(*DeviceInfo).AddManagementState() | Removed |
(*DeviceInfo).addReachabilityState() | Removed (was unexported) |
(*DeviceInfo).addIdentifier() | Replaced by AddMacIdentifier() (now exported) |
(*DeviceInfo).AddSoftware() | Renamed to AddSoftwareArtifactComponent() |
model.getAssetContext() | Removed |
model.getAssetCreationTimestamp() | Removed |
model.GatewayHelper* (gateway-helper.go) | File deleted |
config.IdentifiersRequest | Replaced by config.DeviceInfoRequest |
config.NewIdentifiersRequestFromGetIdentifiersReq() | Removed |
features.Identifiers interface | Replaced by features.DeviceInfo |
internal/server/devicediscovery/identifiers.go | Replaced by deviceinfo.go |
generated/conn_suite_drv_info (IdentifiersApiServer) | No longer embedded in alFeatureBuilder |
8. New types and functions
| New symbol | Description |
|---|---|
model.ValidationError | Returned when a field value fails pattern validation |
model.EmptyError | Returned when a required field is empty |
model.PermissibleValuesError | Returned when a value is not in the allowed set |
model.ErrValidation, model.ErrEmpty | Sentinel values for use with errors.Is (see below) |
model.FunctionalObjectSchemaUrl | Constant: canonical schema URL for IAH base schema v1 |
model.MacAddressPattern | Regex pattern for MAC address validation |
model.IPv4AddressPattern | Regex pattern for IPv4 address validation |
model.NetworkMaskPattern | Regex pattern for network mask validation |
model.IPv6AddressPattern | Regex pattern for IPv6 address validation |
model.IPv6NetworkPrefixPattern | Regex pattern for IPv6 network prefix (CIDR notation) |
model.RouterIPv4AddressPattern | Regex pattern for IPv4 router/gateway addresses |
model.RouterIPv6AddressPattern | Regex pattern for IPv6 router/gateway addresses |
model.IdLinkPattern | Regex pattern for IEC 61406 product links |
model.CustomIdentifierValuePattern | Regex pattern for custom identifier values |
model.PredicatePattern | Regex pattern for asset relation predicates |
(*DeviceInfo).AddMacIdentifier(mac) | Exported helper (previously addIdentifier, unexported) |
(*DeviceInfo).AddIdLinkIdentifier(uri) | Add an IdLink identifier |
(*DeviceInfo).AddCustomIdentifier(name, value) | Add a named custom identifier |
(*DeviceInfo).AddCertificateIdentifier(certID) | Add a certificate identifier |
(*DeviceInfo).AddHostBasedSoftwareIdentifier(name, version, hostIdentifier) | Add a host-based software identifier |
(*DeviceInfo).AddParentRelativeIdentifier(parentIdentifier, slot, subslot) | Add a slot/subslot identifier relative to a parent asset |
(*DeviceInfo).AddAssetRelation(...) | Add a relationship to another asset |
(*DeviceInfo).AddNicWithoutMacIdentifier(name, mac) | Add a NIC without auto-adding a MAC identifier |
(*DeviceInfo).AddRunningSoftwareComponent(name, version, isFirmware, runningSoftwareId) | Add a running software component (instance identified by runningSoftwareId) |
(*DeviceInfo).AddProductInstanceIdentifier(vendor, articleNumber, serialNumber) | Add a vendor/article-number/serial-number identifier; skipped only when all three fields are empty |
model.AssetRelation | New type representing a directed asset relationship |
model.RelatedAsset | Asset stub used in AssetRelation |
model.ProductInstanceInformation | Replaces ProductSerialIdentifier |
features.DeviceInfo interface | Replaces features.Identifiers |
generated/conn_suite_device_info package | New gRPC package for the DeviceInfo API |
(*DeviceInfo).ConvertToPropertyValueResults() | Converts a DeviceInfo to []*PropertyValueResult for GetPropertyValues responses |
(*DeviceInfo).ConvertToJson() | Marshals DeviceInfo to map[string]interface{}; used internally by ConvertToPropertyValueResults |
Error type usage with errors.Is and errors.As
Use errors.Is with the provided sentinels to check the error category without needing the details:
import (
"errors"
"github.com/industrial-asset-hub/asset-link-sdk/v4/model"
)
deviceInfo, err := model.NewDevice(string(model.DeviceFunctionalObjectTypeDevice), name)
if err != nil {
switch {
case errors.Is(err, model.ErrEmpty):
log.Warn().Msgf("required field empty: %v", err)
case errors.Is(err, model.ErrValidation):
log.Warn().Msgf("validation failed: %v", err)
default:
return err
}
}
Use errors.As when you need the structured error fields:
deviceInfo, err := model.NewDevice(string(model.DeviceFunctionalObjectTypeDevice), name)
if err != nil {
var emptyErr *model.EmptyError
var permErr *model.PermissibleValuesError
switch {
case errors.As(err, &emptyErr):
log.Warn().Msgf("required field empty: %s", emptyErr.Field)
case errors.As(err, &permErr):
log.Warn().Msgf("invalid value for %s: %v", permErr.Field, permErr.Value)
default:
return err
}
}
Note: model.ErrPermissibleValues is not defined; use errors.As with *model.PermissibleValuesError to match that type.
9. Registry interface constant rename
Before:
registryclient.INTERFACE_COMMON_IDENTIFIERS_V1 // "siemens.common.identifiers.v1"
After:
registryclient.INTERFACE_CONN_SUITE_DEVICEINFO_V1 // "siemens.connectivitysuite.deviceinfo.v1"
10. New generated package: conn_suite_device_info
A new gRPC package has been added at generated/conn_suite_device_info. It contains the protobuf-generated code for the new DeviceInfo API including:
DeviceInfoApiServer/DeviceInfoApiClientinterfacesGetPropertyValuesRequest/GetPropertyValuesResponseGetSupportedPropertiesRequest/GetSupportedPropertiesResponseSupportedPropertywith typedDatatypefield
The proto definitions are in specs/conn_suite_device_info.proto and specs/common_properties.proto.
11. Dependency updates
| Dependency | v3.7.4 | v4.1.x |
|---|---|---|
github.com/Masterminds/semver/v3 | v3.4.0 | v3.5.0 |
github.com/gin-contrib/logger | v1.2.6 | v1.2.7 |
github.com/rs/zerolog | v1.34.0 | v1.35.1 |
golang.org/x/net | v0.52.0 | v0.57.0 |
golang.org/x/term | v0.41.0 | v0.45.0 |
google.golang.org/grpc | v1.79.3 | v1.82.1 |
github.com/quic-go/quic-go | v0.59.0 | v0.59.1 |
Quick migration checklist
- Update
go.modmodule path:v3→v4 - Update all import paths:
asset-link-sdk/v3/...→asset-link-sdk/v4/... - Replace
Identifiers(impl)withDeviceInfo(impl)in the builder chain - Implement
GetPropertyValuesandGetSupportedPropertiesinstead ofGetIdentifiers- Use
request.GetDevice().GetConnectionParameterSet()to get connection parameters - Build a
DeviceInfofrom the retrieved device details - Call
deviceInfo.ConvertToPropertyValueResults()to produce the response
- Use
- Update
registry.json: replace"siemens.common.identifiers.v1"with"siemens.connectivitysuite.deviceinfo.v1"in theapp_typesarray - Update
model.NewDevice(...)call sites to handle the returnederror - Update
AddNameplate(...): renamemanufacturerProductDesignationargument toproductFamily, addproductTypeargument, handle returnederror - Update
AddNic(...): handle the returnederror(always validates MAC format) - Update
AddIPv4(...),AddIPv6(...): handle the returnederror; note that error fires only when all address fields are empty — passing an empty router address while IP and mask are set still succeeds - Update
AddSoftware(...)toAddSoftwareArtifactComponent(...), handle returnederror - Use
AddProductInstanceIdentifier(vendor, articleNumber, serialNumber)for product instance identifiers (replaces manualProductSerialIdentifierconstruction) - Update
AddDescription(...),AddCapabilities(...): handle returnederror - Remove any references to
ManagementState,ReachabilityState,GatewayInfo,AssetContext - Remove any references to
ProductInstanceIdentifier; useProductInstanceInformationviaAddNameplate - Add optional
Description,DocUrl,FeedbackUrlfields tometadata.Metadata(optional but recommended) - Run
go mod tidyandgo build ./...to confirm no remaining compilation errors