schemaversionmediator

package
v1.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2026 License: MIT Imports: 16 Imported by: 0

README

SchemaVersionMediator Plugin

schemaversionmediator mediates schema version differences between Beckn participants. When a BAP and a BPP declare different schema object versions in their node manifests, this plugin fetches translation artifacts from the network's artifact registry, executes them, and patches the payload so that each side receives data in the schema version it expects.


Table of Contents

  1. How It Works
  2. Plugin ID and Dependencies
  3. Configuration Reference
  4. Step Ordering
  5. Direction Awareness
  6. Cold-Start Behaviour
  7. Node Manifest Schema
  8. Error Codes
  9. Translation Artifacts
  10. Data-Loss Detection
  11. Known Limitations

How It Works

On every inbound request, Mediate runs the following sequence:

  1. Cold-start guard — if the local node manifest was absent, unreachable, or had no schemaObjects at startup (or if nodeId was not set in config), every call is rejected immediately with SCH_SUBSCRIBER_NOT_FOUND.
  2. Identity extraction — reads networkId/network_id from the payload context block; reads the counterparty subscriber ID from ContextKeyRemoteID (set by reqpreprocessor). If either is empty the payload passes through unchanged.
  3. Target manifest selection — direction-aware:
    • Receiver handler (bapTxnReceiver, bppTxnReceiver): uses the local node manifest loaded at startup. No network call at request time.
    • Caller handler (bapTxnCaller, bppTxnCaller): calls ManifestLoader.GetBySubscriberID to fetch the counterparty's node manifest from DeDi at request time.
  4. Compatibility check — walks the payload for @context+@type pairs and compares them against the target manifest's schemaObjects. If all objects are at a supported version, the payload passes through unchanged.
  5. Artifact fetch — for each incompatible schema object, derives the artifact URL from the target manifest's baseUrl, canonical version, and the source version, then fetches it over HTTP.
  6. Translation — composes the fetched artifacts into a single JSONata expression ($merge([$, patch1, patch2, ...])) and executes it against the message subtree of the payload in one pass.
  7. Data-loss detection — compares flattened key paths of the source and translated message. If any source keys are absent in the output, the request is rejected with SCH_SCHEMA_ADAPTATION_FAILED.
  8. Patch — replaces the message field in ctx.Body with the translated output.

Plugin ID and Dependencies

plugins:
  manifestLoader:
    id: manifestloader          # required — backed by dediregistry
  schemaVersionMediator:
    id: schemaversionmediator
    config:
      nodeId: "nfh.global/subscribers.beckn.one/open-kitchen-bpp"  # required
      action: translate
      onFailure: reject

Required dependencies:

Dependency Why
manifestLoader Fetches counterparty node manifests from DeDi at request time (caller path) and the local manifest at startup
dediregistry (backing the manifestLoader) Resolves subscriber manifest URLs via DeDi
reqpreprocessor (middleware) Extracts the counterparty subscriber ID from the inbound Authorization header and stores it in ContextKeyRemoteID. Without it, counterpartyID is empty and mediation is skipped with a warning on every request.

nodeId is an operator-facing config field set under schemaVersionMediator.config. It is the three-part DeDi subscriber identity for this node (namespace/registry/recordId, e.g. nfh.global/subscribers.beckn.one/open-kitchen-bpp). At startup the plugin calls ManifestLoader.GetBySubscriberID(nodeId) to load the local node manifest. If nodeId is absent or the manifest cannot be loaded, the mediator marks itself as notOnboarded and rejects every request.


Configuration Reference

Key Values Default Description
nodeId string Required. Three-part DeDi subscriber identity for this node (namespace/registry/recordId). Used at startup to load the local node manifest.
action translate | reject translate What to do when schema objects are incompatible
onFailure reject | passThrough reject What to do when action=translate but an artifact cannot be fetched
fetchTimeout duration string "30s" HTTP timeout for each artifact fetch (e.g. "10s", "1m")
artifactCacheTTL duration string "24h" How long to cache successfully fetched artifacts
negativeCacheTTL duration string "5m" How long to cache artifact-not-found responses
maxCacheEntries integer string "500" Maximum number of entries in the artifact cache

action values:

  • translate — attempt translation for each incompatible schema object; apply onFailure if any artifact is unavailable.
  • reject — return SCH_SCHEMA_ADAPTATION_FAILED immediately without attempting translation.

onFailure values (only evaluated when action=translate):

  • reject — return SCH_SCHEMA_ADAPTATION_FAILED when an artifact cannot be fetched.
  • passThrough — forward the untranslated payload. Operator escape hatch for false-mismatch situations (e.g. stale local manifest during rollout). Not recommended for production.

Step Ordering

validateSchema → mediateSchema is the correct order for both caller and receiver handler configs.

steps:
  - validateSign
  - validateSchema      # validate source payload in its declared schema version
  - mediateSchema       # translate domain schema objects if versions differ
  - addRoute

Prerequisite — reqpreprocessor middleware: mediateSchema reads the counterparty subscriber ID from ContextKeyRemoteID, which is set by the reqpreprocessor middleware. This middleware must appear in the handler's middleware list (it runs before any step). If it is absent, ContextKeyRemoteID will be empty and every Mediate call will log a warning and pass through without translating.

Why this order works universally:

Schema definitions are publicly available and versioned. The schema validator resolves @context URLs directly, so it can validate any version payload regardless of what the local node runs. Translating before validation would leave @context URLs pointing at the source version while the field structure reflects the target — an inconsistency that extended schema validation would misread.

Artifact authoring note: Translation expressions should update @context URLs as part of the translation to keep the translated payload internally consistent. This is a requirement for translation artifact authors, not enforced by the plugin.


Direction Awareness

Mediate behaves differently depending on whether the handler is a caller or a receiver. The distinction is carried by StepContext.IsCallerHandler.

Handler type Examples Target manifest Manifest source
Receiver bapTxnReceiver, bppTxnReceiver Local node manifest Loaded once at startup via nodeId
Caller bapTxnCaller, bppTxnCaller Counterparty node manifest Fetched per-request via ManifestLoader.GetBySubscriberID

Receiver path: the plugin checks whether the inbound payload's schema versions are compatible with what this node expects. Translation makes the inbound payload match the local node's declared versions.

Caller path: the plugin checks whether the outbound payload's schema versions are compatible with what the counterparty expects. Translation makes the outbound payload match the counterparty's declared versions before forwarding.

Both paths use the same compatibility check, artifact fetch, and translation logic — only the manifest source differs.


Cold-Start Behaviour

At startup, New calls ManifestLoader.GetBySubscriberID using the nodeId config value to load the local node manifest. The mediator sets an internal notOnboarded flag if any of the following occur:

  • nodeId is absent or empty in config
  • The manifest document cannot be fetched or parsed
  • The manifest contains no schemaObjects

While notOnboarded is set, every Mediate call returns SCH_SUBSCRIBER_NOT_FOUND immediately. The adapter must be restarted after the local node manifest is published to DeDi and nodeId is correctly set in config.


Node Manifest Schema

The node manifest is a YAML file that declares the schema types a participant supports and their accepted versions. It is the primary input to compatibility checking.

Full structure
manifestVersion: "1.0"
manifestType: "node-manifest"
subscriberId: "nfh.global/subscribers.beckn.one/open-kitchen-bpp"

schema:
  defaultVersionPolicy: "latest"   # optional — applied to objects that don't set their own
  schemaObjects:
    - type: "RetailConsideration"
      baseUrl: "https://raw.githubusercontent.com/beckn/local-retail/refs/heads/main/schema/RetailConsideration"
      supportedVersions:
        - "v2.1"
        - "v2.2"
      versionPolicy: "pinned"       # optional — overrides defaultVersionPolicy for this object
      pinnedVersion: "v2.2"         # required when versionPolicy=pinned

    - type: "RetailOffer"
      baseUrl: "https://raw.githubusercontent.com/beckn/local-retail/refs/heads/main/schema/RetailOffer"
      supportedVersions:
        - "v2.1"

governance:
  effectiveFrom: "2024-01-01T00:00:00Z"
  effectiveUntil: "2027-01-01T00:00:00Z"  # optional — omit for indefinite validity
schema section
Field Required Description
defaultVersionPolicy No Applied to all schemaObjects that do not set their own versionPolicy. Values: latest (default) or pinned.
schemaObjects Yes List of schema type declarations. At least one entry required for the mediator to consider the node onboarded.
schemaObjects entries
Field Required Description
type Yes Schema type name as it appears in @type in the Beckn payload (e.g. RetailConsideration). Must match exactly — case-sensitive.
baseUrl Yes Base URL prefix for this schema type. The mediator appends /{version}/context.jsonld to form the context URL, and /{canonicalVersion}/{Type}_from_{fromVersion} to derive artifact URLs.
supportedVersions Yes List of schema versions this node handles natively. Payloads at any listed version are considered compatible — no translation triggered.
versionPolicy No Controls which version is the canonical translation target. latest selects the highest version in supportedVersions by major.minor comparison. pinned uses pinnedVersion. Defaults to defaultVersionPolicy.
pinnedVersion No Required when versionPolicy: pinned. Must be one of the versions in supportedVersions.
governance section
Field Required Description
effectiveFrom Yes RFC 3339 timestamp from which the manifest is valid. Manifests with a future effectiveFrom are rejected at load time.
effectiveUntil No RFC 3339 timestamp at which the manifest expires. Omit for indefinite validity. Expired manifests are rejected at load time.
Artifact URL derivation

When a payload carries a schema object at a version not in supportedVersions, the mediator constructs the artifact URL as:

{baseUrl}/{canonicalVersion}/{type}_from_{fromVersion}

For example, if RetailConsideration has baseUrl: https://.../schema/RetailConsideration, supportedVersions: [v2.2], and the inbound payload declares v2.1:

https://.../schema/RetailConsideration/v2.2/RetailConsideration_from_v2.1

The artifact at that URL must contain a JSONata expression that transforms a v2.1 message structure into a v2.2-compatible one.


Error Codes

All errors returned by Mediate are of type *MediationError, which carries a Code field aligned with the Beckn v2.0.0 ErrorCode taxonomy's SCH_* prefix. MediationError implements model.BecknErrorer, so the handler's NACK-building dispatch (nackBecknError) recognizes it and surfaces its Code/Message as a proper NACK response, at HTTP 400 Bad Request.

Code Cause Resolution
SCH_SUBSCRIBER_NOT_FOUND Local node manifest absent or has no schemaObjects at startup Publish node manifest to DeDi and restart the adapter
SCH_SCHEMA_ADAPTATION_FAILED Two causes share this code: (1) incompatible schema objects found and action=reject, or artifact fetch failed and onFailure=reject; (2) translation dropped fields present in the source payload (not yet implemented — no code path currently constructs this case) Check counterparty manifest in DeDi and verify artifact URLs are reachable; for (2), review the translation artifact — it must not remove fields from the source

Plain (non-MediationError) errors may also be returned for malformed payloads (e.g. missing message field). The handler treats these as HTTP 400 Bad Request with a generic error body — distinct from the structured NACK produced by MediationError.


Translation Artifacts

Translation artifacts are external files fetched at runtime from URLs derived from the counterparty's node manifest contextUrl field. The artifact URL is constructed by replacing the schema version segment in the contextUrl path.

Currently supported content type: application/jsonata.

Artifacts are cached in memory with configurable positive and negative TTLs. The cache is per-mediator-instance and is not shared across handlers.


Data-Loss Detection

After translation, the plugin compares the flattened dot-notation key paths of the source message subtree against the translated output. Any key present in the source but absent in the output is considered a dropped field.

Current behaviour: data loss always causes rejection with SCH_SCHEMA_ADAPTATION_FAILED, listing the dropped field paths. There is no configurable policy — this is intentional. A partially translated payload is as harmful as an incompatible one.

Array handling: array elements are treated as opaque leaf values. Element-level drops within an array are not detected — only object key presence is compared.


Known Limitations

  • Non-JSONata translator types are not yet supported. The translation dispatch layer is in place; additional content types will be wired in future releases.
  • Observed seeding (auto-updating the local node manifest from live traffic) is not implemented. Tracked in #822.
  • RunOnResponse is not implemented — Beckn responses arrive as separate inbound requests and are mediated by Mediate on the receiver handler, not via a response hook.

Documentation

Overview

Package schemaversionmediator implements the SchemaVersionMediator plugin. It walks inbound Beckn payloads, checks schema object compatibility against the local node manifest, and dispatches translation for incompatible objects.

Index

Constants

This section is empty.

Variables

View Source
var ErrArtifactNotFound = errors.New("schemaversionmediator: translation artifact not found")

ErrArtifactNotFound is returned by fetchArtifact when no translation artifact exists at the derived URL (HTTP 404). Distinct from transient network errors so the mediation loop can apply OnFailure policy for "map doesn't exist yet" vs "registry unreachable".

View Source
var ErrNoManifest = errors.New("schemaversionmediator: node manifest unavailable, skipping mediation")

ErrNoManifest is returned by CheckCompatibility when the node manifest is nil. The caller should log a warning and skip mediation — translation targets cannot be determined without a manifest, but the absence of one is not a hard failure.

Functions

func ComposeExpression

func ComposeExpression(entries []MappingEntry) (string, error)

ComposeExpression combines N schema-object-level patch expressions into a single JSONata expression evaluated at the message root via $merge.

This function is NOT called by the mediator's translation loop. The mediator executes each artifact against its own subtree independently. ComposeExpression is provided for callers that need to assemble a single composed expression for testing or non-standard evaluation outside the mediation pipeline.

An empty entries list returns the identity expression "$". The returned string can be compiled and evaluated by Execute.

func New

New is the package-level constructor used by the plugin entrypoint.

Types

type ArtifactFetchFailure

type ArtifactFetchFailure struct {
	Need   TranslationNeeded
	URL    string // artifact URL that was attempted; empty when URL derivation failed
	Reason error
}

ArtifactFetchFailure records a single failed artifact fetch with the full context needed for a structured log event: which schema object was being translated, from/to what version, which URL was attempted, and why it failed.

type MappingEntry

type MappingEntry struct {
	JSONataPath string // from WalkPayload, e.g. "$.message.fulfillment"
	Expression  string // JSONata expression scoped to the schema object subtree
}

MappingEntry pairs a translation artifact expression with the payload path of the schema object it targets.

NOTE: MappingEntry and ComposeExpression are NOT used by the mediation hot path. The mediator executes each artifact expression independently against its own schema object subtree (via getAtPath/setAtPath) so that artifact authors write expressions scoped to the object, not the message root. MappingEntry and ComposeExpression are retained as tested utilities for callers that want to assemble and evaluate a composed message-root patch expression outside the normal mediation flow.

If you are writing a translation artifact: express it relative to the schema object itself (e.g. `$ ~> |$|{"discountCode": "NONE"}|`), not relative to the message root.

type MediationError

type MediationError struct {
	Code          string
	Message       string
	DroppedFields []string // non-nil only for SCH_SCHEMA_ADAPTATION_FAILED (data-loss variant)
	// contains filtered or unexported fields
}

MediationError is a structured rejection returned by Mediate. It carries a camelCase error code and a human-readable message so the handler can build a Beckn NACK response with the correct fault details. cause is the underlying technical error; it is available via errors.Unwrap for logging but is not exposed in the user-facing Message.

func (*MediationError) BecknError added in v1.8.1

func (e *MediationError) BecknError() *model.Error

BecknError converts the MediationError into the shared *model.Error NACK payload, implementing model.BecknErrorer so nackBecknError (core/module/handler/responsestep.go) surfaces this Code/Message on the wire instead of falling through to a generic 500 Internal Server Error.

func (*MediationError) Error

func (e *MediationError) Error() string

func (*MediationError) Unwrap

func (e *MediationError) Unwrap() error

type PayloadRef

type PayloadRef struct {
	ContextURL string
	Type       string
}

PayloadRef is the schema identity as extracted from the wire payload: the raw @context URL and @type value found at a specific node in the payload. It is distinct from model.SchemaObject, which is the manifest declaration.

type PolicyAction

type PolicyAction string

PolicyAction defines what the mediator does when schema incompatibility is detected or when a translation attempt fails.

const (
	// PolicyActionReject rejects the request immediately with a NACK.
	PolicyActionReject PolicyAction = "reject"
	// PolicyActionTranslate attempts translation for each incompatible schema
	// object. On failure the OnFailure policy applies.
	PolicyActionTranslate PolicyAction = "translate"
	// PolicyActionPassThrough forwards the request as-is with a structured log
	// signal. Valid only as onFailure — used when no artifact is published yet
	// and the operator accepts the risk of forwarding an untranslated payload.
	PolicyActionPassThrough PolicyAction = "passThrough"
)

type SchemaObjectRef

type SchemaObjectRef struct {
	PayloadRef
	JSONataPath string
}

SchemaObjectRef is a PayloadRef annotated with the JSONata dot-notation path to its location in the payload (e.g. "$.message.order"). The path flows through to TranslationNeeded for logging and debugging; it is not interpreted by ComposeExpression.

func WalkPayload

func WalkPayload(payload []byte) (refs []SchemaObjectRef, skipped []string, err error)

WalkPayload recursively traverses a JSON payload and returns all schema objects declared via JSON-LD "@context" and "@type" fields, each annotated with the JSONata path to its location in the tree. The walk is depth-first and collects every qualifying node regardless of nesting level, including both a parent node and its nested children when both carry "@context"/"@type" declarations — each is an independent schema contract. The payload is not modified. WalkPayload recursively traverses a JSON payload and returns all schema objects declared via JSON-LD "@context" and "@type" fields, each annotated with its JSONata path from the payload root (e.g. "$.message.offer"). It collects both a parent node and its nested children when both carry "@context"/"@type" pairs. Also returns skipped paths — nodes that have "@context" but no "@type" — so callers can log warnings for misconfigured payloads without silently ignoring them.

type TranslationArtifact

type TranslationArtifact struct {
	Content     []byte
	ContentType string
}

TranslationArtifact holds a fetched translation artifact and the Content-Type returned by the server. ContentType determines which Translator implementation the mediation loop dispatches to (e.g. "application/jsonata").

type TranslationNeeded

type TranslationNeeded struct {
	From             PayloadRef
	To               *model.SchemaObject
	CanonicalVersion string
	JSONataPath      string
}

TranslationNeeded describes a single payload schema object that requires translation.

From is the schema identity as declared in the payload. To is the manifest entry this node supports for the same type. CanonicalVersion is the resolved translation target version from To's policy — pre-computed at CheckCompatibility time so deriveArtifactURL needs no manifest context. JSONataPath is the payload path forwarded from SchemaObjectRef.

func CheckCompatibility

func CheckCompatibility(extracted []SchemaObjectRef, manifest *model.NodeManifest) ([]TranslationNeeded, error)

CheckCompatibility compares extracted schema object refs against the local node manifest and returns those that require translation. An empty result means the payload is fully compatible and the mediator can short-circuit.

Returns ErrNoManifest if manifest is nil — the caller should log a warning and skip mediation rather than treating this as a hard failure.

For each extracted SchemaObjectRef:

  • Exact match in manifest → compatible, omitted from result.
  • Same Type, different ContextURL → TranslationNeeded with To set to the locally supported SchemaObject (version the node expects).
  • Type absent from manifest entirely → TranslationNeeded with To nil; handling is delegated to the data-loss policy enforcer.

The JSONataPath from each ref is forwarded into TranslationNeeded for the caller's logging and debugging use; it is not interpreted by ComposeExpression.

func (TranslationNeeded) ToContextURL

func (t TranslationNeeded) ToContextURL() string

ToContextURL returns the canonical @context URL for the translation target: {To.BaseURL}/{CanonicalVersion}/context.jsonld.

type TranslationPolicy

type TranslationPolicy struct {
	Action    PolicyAction
	OnFailure PolicyAction
}

TranslationPolicy governs mediator behaviour when schema incompatibilities are found. It is loaded from the plugin config map and applied by Mediate.

Action is evaluated immediately after CheckCompatibility returns incompatible objects — before any translation is attempted. OnFailure is only consulted when Action is PolicyActionTranslate and the translation attempt fails (no artifact found, or execution error).

Directories

Path Synopsis
Package main provides the plugin entry point for the SchemaVersionMediator plugin.
Package main provides the plugin entry point for the SchemaVersionMediator plugin.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL