Skip to content

Introduction

The GeoKrety system manages physical trackable items identified by unique IDs in two representations:

  1. Internal ID (int64): Database-internal identifier used exclusively for foreign keys and internal references
  2. Public GKID (string): User-facing identifier following the format GK + hexadecimal value (e.g., GK0001, GKB65C, GKFFFF)

The GeoKrety Stats API exposes GKID fields through a centralized GeokretId type instead of scattered ad hoc conversions. This specification defines the contract for that type, including bidirectional conversion between internal and public ID representations, JSON/XML serialization and deserialization, and consistent parsing rules across the API.

1. Purpose & Scope

Purpose: Define a robust, encapsulated GeokretId type that centralizes GeoKret ID conversion logic, ensures consistent handling across all API boundaries, and reduces error-prone manual conversions.

Scope:

  • Type definition and constructor methods
  • Conversion algorithms (int ↔ string)
  • JSON marshaling/unmarshaling behavior
  • XML marshaling/unmarshaling behavior
  • Nullable handling using Go's pointer semantics
  • Package placement in a reusable geokrety Go package
  • Integration patterns for handlers, store layer, and API response entities
  • Testing strategy and validation
  • Migration path for existing code

Intended Audience: Backend API developers, systems architects, and code reviewers working with the GeoKrety Stats API codebase.

Assumptions:

  • GeoKrety IDs are always positive non-zero integers (1–4294967295 range suitable for 32-bit values)
  • Public GKID format uses uppercase hexadecimal with leading zeros (4 hex digits minimum)
  • The type will be used in all places where GKID values appear in API responses
  • Existing database schema remains unchanged (stores integer gkid values)

2. Definitions

Term Definition
Internal ID 64-bit signed integer stored in database for foreign key relationships; not directly exposed to users
Public GKID User-facing representation of a GeoKret identifier in format GK followed by uppercase hexadecimal digits (e.g., GK0001, GKB65C)
Marshaling Process of converting a Go value to JSON or XML format for API responses
Unmarshaling Process of parsing JSON or XML input to populate Go values
XML Element In XML representation, the GKID string maps to either an XML attribute or element content
Nullable/Optional A value that may or may not be present; represented as *GeokretId (pointer) in Go
Validation Ensuring a value conforms to defined constraints (positive, within range, correct format)
Zero Value The default value of a type when not explicitly initialized (for pointers, this is nil)

3. Requirements, Constraints & Guidelines

Functional Requirements

  • REQ-001: The GeokretId type shall encapsulate an internal int64 value and provide safe access through methods
  • REQ-002: The type shall convert internal int64 to public GKID string format (GK + uppercase hex; minimum 4 hex digits with zero-padding)
  • REQ-003: The type shall parse public GKID strings (with or without GK prefix, case-insensitive) to create valid instances
  • REQ-004: The type shall also accept plain decimal integer input for backward compatibility with legacy systems; digit-only strings without a leading zero are treated as decimal, while zero-padded digit-only strings are treated as hexadecimal
  • REQ-005: JSON marshaling shall serialize GeokretId to public GKID string format
  • REQ-006: JSON unmarshaling shall accept public GKID strings and decimal integers; JSON null leaves pointer fields unset through the standard encoder path
  • REQ-007: Nil pointers (*GeokretId) shall marshal to JSON null without error
  • REQ-007b: XML marshaling shall serialize GeokretId to public GKID string format (as element or attribute)
  • REQ-007c: XML unmarshaling shall accept public GKID strings and decimal integers from XML elements or attributes; absent XML elements leave pointer fields unset through the standard decoder path
  • REQ-007d: Nil pointers (*GeokretId) shall omit the XML element or attribute without error
  • REQ-008: The type shall validate that values are positive non-zero integers
  • REQ-009: Constructor and parsing functions shall return errors for invalid inputs with descriptive messages
  • REQ-010: Go string conversion via fmt.Stringer interface shall return public GKID format
  • REQ-011: Go string formatting (%v, %s) shall display public GKID by default
  • REQ-011b: Go string formatting with %d shall display the integer representation of the GKID

Non-Functional Requirements

  • NFR-001: Type operations (conversion, validation) shall complete in O(1) time
  • NFR-002: No memory allocations beyond the pointer/value itself in happy-path scenarios
  • NFR-003: Error messages shall be descriptive and aid developers in debugging invalid inputs
  • NFR-004: Type shall be compatible with pgx and SQL scanning operations (nullable field support)
  • NFR-005: XML marshaling/unmarshaling shall have comparable performance to JSON marshaling

Constraints

  • CON-001: The internal integer value must remain invariant after creation; immutability is strongly recommended
  • CON-002: Only non-zero positive integers shall be accepted (1 to 2^63-1 technically, but practically 1 to 2^32-1 per GeoKrety convention)
  • CON-003: Public GKID format is fixed: GK + exactly 4 uppercase hex digits minimum, no alternatives
  • CON-004: The zero value (uninitialized GeokretId{}) is invalid; deliberate construction via New() or FromInt() is required
  • CON-005: Pointer receivers (*GeokretId) must handle nil gracefully in helper and formatting methods; core accessors such as Int() and ToGKID() may panic on nil as documented below
  • CON-006: No constructor should accept or validate values above typical GeoKrety ranges; range checking is the responsibility of business logic

Guidelines

  • GUD-001: Always use New() or FromInt() constructors rather than struct literal initialization
  • GUD-002: Prefer *GeokretId in API response structs for optional fields; use non-pointer GeokretId only when the field is guaranteed to exist
  • GUD-003: Use the String() method or %v formatting for logging and debugging; do not manually call ToGKID() for display
  • GUD-004: Validate GeoKrety ID values immediately upon input (in handlers) before passing to store or business logic layers
  • GUD-005: Store methods should continue to accept int64 parameters; conversion happens at API boundary (handler layer)
  • GUD-006: Document which fields in structs use GeokretId vs. raw int64 to clarify when automatic conversion applies

4. Interfaces & Data Contracts

Type Definition

The implementation lives in gkid.go under /home/kumy/GIT/geokrety-stats/geokrety/geokrety, inside a dedicated reusable geokrety Go module. Related helper packages live alongside it in /home/kumy/GIT/geokrety-stats/geokrety/geokrety and /home/kumy/GIT/geokrety-stats/geokrety/move.

// GeokretId represents a GeoKret identifier with internal and public representations.
// The zero value is not valid; use New() or FromInt() constructors.
type GeokretId struct {
value int64 // unexported; immutable after creation
}

Constructor Methods

// New creates a GeokretId from a public GKID string (e.g., "GK0001", "GKB65C").
// Accepts formats: "GK00FF" or "gk00ff" (standard, case-insensitive),
// "00FF" (without prefix), and "255" (decimal). Digit-only strings without
// a leading zero are parsed as decimal; zero-padded digit-only strings are
// parsed as hexadecimal.
// Returns an error if the input is invalid or the resulting integer is non-positive.
func New(gkid string) (*GeokretId, error)

// FromInt creates a GeokretId from an internal integer value.
// Returns an error if the value is non-positive or invalid.
func FromInt(v int64) (*GeokretId, error)

// NewNullable creates a nullable GeokretId from a public GKID string.
// Returns nil if the input is empty or whitespace; otherwise behaves like New().
func NewNullable(gkid string) (*GeokretId, error)

// GeokretTypeName returns the textual GeoKret type label for an integer type code.
func GeokretTypeName(typeID int16) string

// MoveTypeName returns the textual move type label for an integer type code.
func MoveTypeName(typeID int16) string

Accessor and Conversion Methods

// Int returns the internal integer value.
// Panics if receiver is nil.
func (g *GeokretId) Int() int64

// ToGKID returns the public GKID string representation (e.g., "GK0001").
// Panics if receiver is nil.
func (g *GeokretId) ToGKID() string

// String implements the fmt.Stringer interface, returning the public GKID format.
// Returns "nil" for nil receiver (safe for use in logging).
func (g *GeokretId) String() string

// IntOrZero returns the internal integer value, or 0 if receiver is nil.
func (g *GeokretId) IntOrZero() int64

// ToGKIDOrEmpty returns the public GKID, or empty string if receiver is nil.
func (g *GeokretId) ToGKIDOrEmpty() string

JSON Marshaling Interface

// MarshalJSON encodes the GeokretId as a public GKID string in JSON.
// Nil pointer fields marshal as JSON null through the standard encoder path.
// Example output: {"gkid": "GK0001"}
func (g GeokretId) MarshalJSON() ([]byte, error)

// UnmarshalJSON decodes a GeokretId from JSON (string or number).
// Accepts: "GK0001" or "gk0001", "0001" (hex without prefix), and
// 1 (decimal integer). Digit-only strings without a leading zero are decimal;
// zero-padded digit-only strings are hexadecimal.
// Pointer fields remain nil on JSON null through the standard decoder path.
func (g *GeokretId) UnmarshalJSON(data []byte) error

XML Marshaling Interface

// MarshalXML encodes the GeokretId as a public GKID string in XML element.
// Nil pointer fields omit the element through the standard encoder path.
// Example output: <gkid>GK0001</gkid>
func (g GeokretId) MarshalXML(e *xml.Encoder, start xml.StartElement) error

// UnmarshalXML decodes a GeokretId from XML element content (string or number).
// Accepts: "GK0001" or "gk0001", "0001" (hex without prefix), and
// "1" (decimal integer string). Digit-only strings without a leading zero are
// decimal; zero-padded digit-only strings are hexadecimal.
// Pointer fields remain nil when the element is absent through the standard decoder path.
func (g *GeokretId) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error

Entity Integration

Entity structs containing GKID fields shall use *GeokretId instead of *int64:

Before:

type GeokretListItem struct {
    ID       int64   `db:"id" json:"id"`
    GKID     *int64  `db:"gkid" json:"gkid"`
    Name     string  `db:"name" json:"name"`
    // ...
}

After:

type GeokretListItem struct {
    ID       int64        `db:"id" json:"id"`
    GKID     *GeokretId   `db:"gkid" json:"gkid"`
    Name     string       `db:"name" json:"name"`
    // ...
}

Error Contract

All parsing and constructor functions shall return a GeokretIdError with clear context:

type GeokretIdError struct {
    Input  string // the invalid input value
    Reason string // human-readable explanation
}

// Example errors:
// Input: "GK0000", Reason: "gkid must be greater than zero"
// Input: "XYZ123", Reason: "invalid gkid format; expected GK[0-9A-F]+, hexadecimal without prefix, or decimal integer"
// Input: "-5", Reason: "gkid must be positive"

5. Acceptance Criteria

  • AC-001: Given a valid GKID string "GK0001", When creating New("GK0001"), Then a non-nil *GeokretId is returned with ToGKID() = "GK0001"

  • AC-002: Given a valid integer 1, When creating FromInt(1), Then a non-nil *GeokretId is returned with Int() = 1 and ToGKID() = "GK0001"

  • AC-003: Given an integer 255, When creating FromInt(255), Then ToGKID() = "GK00FF" (zero-padded hex)

  • AC-004: Given JSON input {"gkid": "GK0001"}, When unmarshaling into a struct with *GeokretId field, Then the field is populated correctly and Int() = 1

  • AC-005: Given JSON input {"gkid": 1} (plain integer), When unmarshaling, Then it is accepted and converted to *GeokretId with value 1

  • AC-006: Given a *GeokretId with value 255, When marshaling to JSON, Then the output is "GK00FF" (not 255)

  • AC-006b: Given a *GeokretId with value 255, When marshaling to XML, Then the output is <gkid>GK00FF</gkid> (public GKID format in element content)

  • AC-007: Given a nil *GeokretId receiver, When calling String(), Then it returns "nil" without panic

  • AC-008: Given an invalid input "GK0000" (zero value), When calling New(), Then an error is returned with reason mentioning "greater than zero"

  • AC-009: Given an empty or whitespace-only string, When calling NewNullable(), Then nil is returned without error

  • AC-010: Given valid inputs in multiple formats ("GK0001", "gk0001", "0001", "1"), When parsing each format, Then all resolve to the same internal value, with digit-only inputs without a leading zero interpreted as decimal and zero-padded digit-only inputs interpreted as hexadecimal

  • AC-011: Given a legacy endpoint accepting both GKID and plain integer parameters, When the field is populated with *GeokretId, Then both input formats work transparently

  • AC-011b: Given XML input with GKID element, When unmarshaling into struct with *GeokretId field, Then both <gkid>GK0001</gkid> and <gkid>1</gkid> formats work transparently

  • AC-012: Given API response struct with GKID *GeokretId field, When marshaling to JSON, Then the output shows public GKID string (e.g., "gkid": "GK0001"), not the internal integer

  • AC-012b: Given API response struct with GKID *GeokretId field, When marshaling to XML, Then the output shows public GKID string (e.g., <gkid>GK0001</gkid>), not the internal integer

  • AC-013: Given a *GeokretId with value 255, When formatting with fmt.Sprintf("%d", gkid), Then the output is "255"

  • AC-014: Given XML input with gkid as an attribute, When unmarshaling into a struct with *GeokretId field, Then the value is parsed using the same rules as element content

6. Test Automation Strategy

Test Levels & Frameworks

  • Unit Tests: Use Go's standard testing package; focus on constructor, conversion, and edge cases
  • Integration Tests: Test with actual database queries and API routes; use prepared test datasets
  • JSON Serialization Tests: Use encoding/json package to verify marshaling/unmarshaling in both directions
  • XML Serialization Tests: Use encoding/xml package to verify marshaling/unmarshaling in both directions
  • Database Scanning Tests: Verify sql.Scanner and driver.Valuer implementations work with pgx

Test Coverage Areas

Constructor Tests

✓ New() with valid GKID format (e.g., "GK0001")
✓ New() with hex without prefix (e.g., "0001")
✓ New() with decimal integer string (e.g., "1")
✓ New() with zero value → error
✓ New() with negative value → error
✓ New() with invalid format → error
✓ FromInt() with positive integer
✓ FromInt() with zero → error
✓ FromInt() with negative → error
✓ NewNullable() with empty string → nil, no error
✓ NewNullable() with whitespace → nil, no error
✓ NewNullable() with invalid format → error

Conversion Tests

✓ Int() returns correct internal value
✓ ToGKID() formats as GK + zero-padded hex
✓ ToGKID() with small value (e.g., 1 → "GK0001")
✓ ToGKID() with large value (e.g., 65535 → "GKFFFF")
✓ String() returns GKID format
✓ String() on nil receiver returns "nil"
✓ IntOrZero() returns value or 0 for nil
✓ ToGKIDOrEmpty() returns GKID or "" for nil

JSON Marshaling Tests

✓ Marshal *GeokretId → JSON string (e.g., "GK0001")
✓ Marshal nil *GeokretId → JSON null
✓ Unmarshal "GK0001" → *GeokretId with value 1
✓ Unmarshal 1 (integer) → *GeokretId with value 1
✓ Unmarshal null → nil *GeokretId
✓ Unmarshal invalid format → error
✓ Round-trip marshal/unmarshal preserves value

XML Marshaling Tests

✓ Marshal *GeokretId → XML element content (e.g., <gkid>GK0001</gkid>)
✓ Marshal nil *GeokretId → omitted element or empty element
✓ Unmarshal <gkid>GK0001</gkid> → *GeokretId with value 1
✓ Unmarshal <gkid>1</gkid> (decimal) → *GeokretId with value 1
✓ Unmarshal empty or missing element → nil *GeokretId
✓ Unmarshal invalid format in XML → error
✓ Round-trip marshal/unmarshal preserves value
✓ XML attribute parsing

Entity Integration Tests

✓ Scan from database integer row → *GeokretId
✓ Scan null value → nil *GeokretId
✓ HTTP response JSON with GKID field → public format
✓ Entity with multiple *GeokretId fields → all converted

Backward Compatibility Tests

✓ parsePublicGKIDParam() handler function works with GeokretId
✓ Store methods receiving int64 still work (no changes)
✓ Legacy numeric API responses behave correctly

Test Data

Use representative samples:

  • Boundary values: 1, 255, 65535, 2^32-1
  • Special values: high values for stress testing
  • Invalid formats: empty string, null, non-hex characters, zero, negative numbers
  • Case variations: "GK0001", "gk0001", "Gk0001"

CI/CD Integration

  • Run unit tests on every PR
  • Run integration tests in staging environment with test database
  • Verify JSON serialization tests pass before API deployment
  • Generate coverage report; maintain >95% coverage for this type

Coverage Threshold

Minimum 95% code coverage including:

  • All constructor paths
  • All error branches
  • Nil receiver handling
  • Edge cases (max value, zero, negative)

7. Rationale & Context

Why a Type Wrapper?

The current approach of using int64 fields directly scatters conversion logic throughout handler code, leading to:

  • Repeated validation logic
  • Inconsistent error handling
  • Difficulty ensuring JSON serialization formats correctly
  • Lack of type safety (easy to confuse internal ID with public GKID)

A dedicated GeokretId type provides:

  • Single source of truth for conversion logic
  • Type safety at compile time; Go compiler prevents mixing GKID with plain integers
  • Automatic serialization to public format in API responses
  • Centralized validation and error messaging
  • Enhanced readability of code (clear intent that a field uses GKID)

Design Decisions

Pointer-based nullability: Go uses *T to represent optional values. Alternatives (custom Option<T> enum, three-state booleans) are less idiomatic.

Unexported field: The internal value field is unexported to enforce immutability and prevent direct manipulation.

Separate constructors for different input types: New() and FromInt() clearly distinguish source context (user input vs. internal system value).

String() method returns GKID, not formatted with type name: Logging g.String() yields "GK0001" (clean), not "GeokretId(GK0001)" (overly verbose for logs).

Panic on nil receiver in Int() and ToGKID(): These are primary accessors; if a developer calls them on nil, it's a logic error worth surfacing. Convenience methods IntOrZero() and ToGKIDOrEmpty() provide nil-safe alternatives.

JSON unmarshaling accepts both string and integer: Maintains compatibility with legacy clients or systems that may send plain integers instead of GKID strings.

Reusable package placement: The implementation lives in a standalone geokrety package so other modules in this repository can reuse GKID parsing and the type-label helpers without depending on API internals.

8. Dependencies & External Integrations

Data Dependencies

  • DAT-001: PostgreSQL database column geokrety.gk_geokrety.gkid (integer type) — The type reads from and writes to this column via pgx driver
  • DAT-002: API client payloads and responses — External systems expecting GKID strings in API JSON responses

Technology Platform Dependencies

  • PLT-001: Go 1.21+ — Standard library packages used (encoding/json, strconv, fmt); no compatibility guarantees for earlier versions
  • PLT-002: pgx PostgreSQL driver — Database scanning and type conversion integration

Infrastructure Dependencies

  • INF-001: HTTP API router (chi/other) — Handler layer that parses incoming GKID parameters and uses this type
  • INF-002: SQL database layer — Store queries that work with int64 values; type converts at handler boundary

Third-Party Services

  • None directly; this type is self-contained

Compliance Dependencies

  • COM-001: Data validation — Must reject zero and negative values per GeoKrety system rules

9. Examples & Edge Cases

Basic Usage

// import geo "github.com/geokrety/geokrety-stats/geokrety/geokrety"

// Constructor from GKID string
gid, err := geo.New("GK0001")
if err != nil {
    log.Fatal(err)
}
fmt.Println(gid.Int())      // Output: 1
fmt.Println(gid.ToGKID())   // Output: GK0001
fmt.Println(gid)            // Output: GK0001 (via String())

// Constructor from integer
gid2, err := geo.FromInt(255)
if err != nil {
    log.Fatal(err)
}
fmt.Println(gid2.ToGKID())  // Output: GK00FF

// In a struct
type GeokretListItem struct {
    ID      int64       `json:"id"`
    GKID    *GeokretId  `json:"gkid"`    // Auto-marshals to public format
    Name    string      `json:"name"`
}

item := GeokretListItem{
    ID:   123,
    GKID: gid,
    Name: "Test Geokrety",
}

data, _ := json.Marshal(item)
fmt.Println(string(data))
// Output: {"id":123,"gkid":"GK0001","name":"Test Geokrety"}

Nullable Handling

// Nil value
var nilGid *GeokretId
fmt.Println(nilGid)         // Output: nil (safe; no panic)
fmt.Println(nilGid.IntOrZero())      // Output: 0
fmt.Println(nilGid.ToGKIDOrEmpty())  // Output: ""

// In JSON
type Response struct {
    GKID *GeokretId `json:"gkid,omitempty"`
}

resp := Response{GKID: nil}
data, _ := json.Marshal(resp)
fmt.Println(string(data))   // Output: {} (field omitted due to nil)

Error Handling

// Zero value error
gid, err := geo.New("GK0000")
fmt.Println(err)  // Output: gkid must be greater than zero

// Negative value
gid, err := geo.FromInt(-5)
fmt.Println(err)  // Output: gkid must be positive

// Invalid format
gid, err := geo.New("INVALID")
fmt.Println(err)  // Output: invalid gkid "INVALID": invalid gkid format; expected GK[0-9A-F]+, hexadecimal without prefix, or decimal integer

// JSON with null
type Response struct {
    GKID *GeokretId `json:"gkid"`
}
var resp Response
json.Unmarshal([]byte(`{"gkid": null}`), &resp)
fmt.Println(resp.GKID)  // Output: <nil>

// XML with missing element
type XMLResponse struct {
    GKID *GeokretId `xml:"gkid"`
}
var xmlResp XMLResponse
xml.Unmarshal([]byte(`<XMLResponse></XMLResponse>`), &xmlResp)
fmt.Println(xmlResp.GKID)  // Output: <nil>

// XML with element content
xml.Unmarshal([]byte(`<XMLResponse><gkid>GK0001</gkid></XMLResponse>`), &xmlResp)
fmt.Println(xmlResp.GKID.ToGKID())  // Output: GK0001

Input Format Flexibility

// All these inputs represent the same GeoKret (ID = 255)
formats := []string{"GK00FF", "gk00ff", "00FF", "FF", "255"}

for _, f := range formats {
    gid, _ := geo.New(f)
    fmt.Println(gid.ToGKID())  // All output: GK00FF
}

Handler Integration

// Old code (before)
func GetGeokretyByGkId(w http.ResponseWriter, r *http.Request) {
    gkidStr := chi.URLParam(r, "gkid")
    gkid, ok := parsePublicGKIDParam(w, r, "gkid")  // Custom parsing
    if !ok { return }

    // ... fetch from store with int64 gkid
}

// New code (after)
func GetGeokretyByGkId(w http.ResponseWriter, r *http.Request) {
    gkidStr := chi.URLParam(r, "gkid")
    gkid, err := geo.New(gkidStr)  // Centralized, type-safe
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    // ... fetch from store with gkid.Int()
    // API response struct automatically serializes as "GK00FF"
}

Database Scanning

// Store layer (no changes needed)
func (s *Store) FetchGeokretyByGKID(ctx context.Context, gkid int64) (GeokretDetails, error) {
    // ... existing query
}

// Handler layer (bridges to new type)
func (h *StatsHandler) GetGeokretyDetailsByGkId(w http.ResponseWriter, r *http.Request) {
    gkidVal, err := geo.New(chi.URLParam(r, "gkid"))
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    details, err := h.store.FetchGeokretyByGKID(r.Context(), gkidVal.Int())
    // ...response includes GeokretDetails.GKID (*GeokretId) → auto-serialized to JSON or XML
}

XML Response Example

type GeokretListItem struct {
    ID   int64      `xml:"id"`
    GKID *GeokretId `xml:"gkid"`
    Name string     `xml:"name"`
}

item := GeokretListItem{
    ID:   123,
    GKID: NewNullable(255),
    Name: "Test Geokrety",
}

data, _ := xml.Marshal(item)
fmt.Println(string(data))
// Output:
// <GeokretListItem>
//   <id>123</id>
//   <gkid>GK00FF</gkid>
//   <name>Test Geokrety</name>
// </GeokretListItem>

10. Validation Criteria

✓ Type compiles without errors in Go 1.21+

✓ All constructor functions work with representative test inputs

✓ JSON marshaling produces correctly formatted strings ("GK" + hex)

✓ JSON unmarshaling accepts multiple input formats and converts correctly

✓ XML marshaling produces correctly formatted element content ("GK" + hex)

✓ XML unmarshaling accepts multiple input formats and converts correctly

✓ Nil receiver methods (String(), IntOrZero(), etc.) don't panic

✓ Panic-prone methods (Int(), ToGKID()) clearly document invariant (non-nil receiver)

✓ Error messages are descriptive and aid debugging

✓ Type integrates seamlessly with existing HTTP handler code

✓ Type integrates seamlessly with existing entity structs

✓ Database transaction tests show correct round-trip (DB int → GeokretId → JSON/XML → client)

✓ Backward compatibility maintained: existing store methods unchanged

✓ New API responses show public GKID format in JSON and XML

✓ XML attributes and elements both supported (if needed)

✓ Code coverage ≥95% for the type implementation including XML paths