Opxf 0.3.0
dotnet add package Opxf --version 0.3.0
NuGet\Install-Package Opxf -Version 0.3.0
<PackageReference Include="Opxf" Version="0.3.0" />
<PackageVersion Include="Opxf" Version="0.3.0" />
<PackageReference Include="Opxf" />
paket add Opxf --version 0.3.0
#r "nuget: Opxf, 0.3.0"
#:package Opxf@0.3.0
#addin nuget:?package=Opxf&version=0.3.0
#tool nuget:?package=Opxf&version=0.3.0
opxf-dotnet
Strongly-typed .NET library for the Open Product Exchange Format (OPXF) — POCOs, System.Text.Json converters, and cross-file conformance validation.
Overview
OPXF is a PIM-agnostic transit format for moving product data from enterprise PIM systems (Akeneo, Inriver, Struct, Pimcore, Bluestone) into downstream consumers (e-commerce platforms, PDF engines, and other channels). This library provides the .NET foundation for working with OPXF payloads.
An OPXF exchange consists of two files:
| File | Purpose |
|---|---|
model.json |
The Definition Manifest — attribute types, localization rules, value lists, product types, category trees |
data.json |
The Product Payload — products, variants, supporting objects, and assets |
Installation
dotnet add package OPXF
Targets .NET Standard 2.0, .NET 8, and .NET 9 — so it can be consumed from .NET Framework 4.6.1+, .NET Core 2.0+, and modern .NET alike. On .NET Standard 2.0 it pulls in the System.Text.Json package; .NET 8/9 use the in-box version.
Note (next major rework): .NET Standard 2.0 support currently relies on a small set of internal polyfills in
src/OPXF/Compatibility/NetStandardPolyfills.cs— compiler-required types (IsExternalInit,RequiredMemberAttribute,CompilerFeatureRequiredAttribute) for theinit/requiredsyntax, plus convenience wrappers (ToHashSet,KeyValuePair.Deconstruct). For the next version, rework the project to drop these wrappers and adjust the code so it compiles natively against .NET Standard 2.0 and the other targets without polyfills (e.g. replacexs.ToHashSet()withnew HashSet<>(xs)and deconstructingforeachloops with explicitKeyValuePairaccess; reconsider theinit/requiredPOCO shape if the compiler-attribute polyfills are also to be removed).
Usage
Deserialize
using OPXF;
var model = OpxfSerializer.DeserializeModel(File.ReadAllText("model.json"));
var data = OpxfSerializer.DeserializeData(File.ReadAllText("data.json"));
Validate
using OPXF.Validation;
var validator = new OPXFValidator();
var result = validator.Validate(model, data);
if (!result.IsValid)
{
foreach (var error in result.Errors)
Console.WriteLine(error); // [CONF-02] /data/products/0/productTypeId: unknown productTypeId 'apparel'
}
Serialize
string json = OpxfSerializer.SerializeData(data);
Stream-based I/O
For large payloads, use the stream overloads to avoid loading the full JSON into a string:
await using var modelStream = File.OpenRead("model.json");
await using var dataStream = File.OpenRead("data.json");
var model = await OpxfSerializer.DeserializeModelAsync(modelStream);
var data = await OpxfSerializer.DeserializeDataAsync(dataStream);
await using var outStream = File.Create("data.out.json");
await OpxfSerializer.SerializeDataAsync(data, outStream);
Work with attribute values
using OPXF.Data;
using OPXF.Model;
foreach (var product in data.Data.Products)
{
foreach (var (attrId, entry) in product.Attributes ?? [])
{
// Value shape depends on the model's localizable flag:
// localizable: false → string | double | bool | string[] | JsonElement
// localizable: true → IReadOnlyDictionary<string, object> keyed by BCP 47 locale
if (entry.Value is IReadOnlyDictionary<string, object> localeMap)
{
var enValue = localeMap["en-GB"];
}
else
{
var value = entry.Value;
}
}
}
Namespaces
| Namespace | Contents |
|---|---|
OPXF |
OpxfSerializer — entry point for serialization; LocalizedTextExtensions — Resolve/ResolveOr for locale-keyed labels |
OPXF.Model |
OpxfModelFile, ModelDefinition, AttributeDefinition, AttributeType (enum), ProductType, ObjectType, AssetType, CategoryType, Channel, Market, AttributeGroup, SelectValue; ModelLookupExtensions + ModelIndex — id resolvers |
OPXF.Data |
OpxfDataFile, DataPayload, Product, Variant, OpxfObject, Asset, Lifecycle (enum), TransferMode (enum), AttributeMap, AttributeValueEntry, CategoryGroup, CategoryNode |
OPXF.Validation |
OPXFValidator, ValidationResult, ValidationError |
Working with values
Attribute values are stored as object (their shape depends on the model), but you rarely need to cast by hand. AttributeValueEntry exposes typed accessors:
var attrs = product.Attributes!;
double weight = attrs["weight"].AsNumber(); // throws if not a number
bool isNew = attrs["is_new"].AsBool();
string fit = attrs["fit"].AsString(); // select id, link id, text, …
var tags = attrs["tags"].AsStringList(); // text-list / select-list / *-link-list
JsonElement raw = attrs["spec_sheet"].AsJson(); // json attributes
if (attrs["sku"].TryAsString(out var sku)) { /* … */ } // non-throwing variants
// Localizable attributes carry a locale-keyed value:
var name = attrs["name"];
if (name.IsLocalized)
string? enName = (string?)name.ForLocale("en-GB", fallbackLocale: "en-US");
Locale-keyed labels (Label, Description, etc.) have resolution helpers:
string? colour = attr.Label.Resolve("de-DE", fallbackLocale: model.DefaultLocale);
string shown = attr.Label.ResolveOr("de-DE", defaultValue: attr.Id);
And model ids resolve without manual First(...) scans:
var pt = model.Model.ProductTypeById(product.ProductTypeId); // one-off lookup
var def = model.Model.ProductAttributeById("colour");
var index = model.Model.Index(); // O(1) for repeated lookups
foreach (var p in data.Data.Products)
var t = index.ProductType(p.ProductTypeId);
Conformance validation
OPXFValidator.Validate() enforces the cross-file rules from docs/conformance.md — constraints that JSON Schema alone cannot express. Each ValidationError carries:
RuleId— the conformance rule code, e.g.CONF-02Message— human-readable description of the violationPath— RFC 6901 JSON Pointer to the offending element, e.g./data/products/0/productTypeId
OPXFValidator.ValidateModel() validates model internal integrity (CONF-31 through CONF-43, plus the model-side market rules CONF-55..57) without a data file.
Validators collect all violations in a single pass — a payload returns all errors at once, not just the first.
Transfer mode
opxf.transferMode is required and modeled as the TransferMode enum (Snapshot / Incremental) — a missing or unknown value is a deserialization error, not a silent default. When Incremental, referential existence rules (CONF-28, CONF-29, CONF-38) are relaxed — missing targets are the exporter's responsibility. Snapshot enforces them fully.
Conformance rules implemented
Section 1 — Model/Data binding
| Rule | Description |
|---|---|
| CONF-01 | opxf.modelId in the data file must equal model.id in the model file |
Section 2 — Products
| Rule | Description |
|---|---|
| CONF-02 | product.productTypeId must reference a defined product type |
| CONF-03 | Every product.categories entry must reference a defined category node |
| CONF-04 | Product attribute ids must be declared in productType.productAttributes |
| CONF-05 | No two products may share the same id |
Section 3 — Variants
| Rule | Description |
|---|---|
| CONF-06 | Variant attribute ids must be declared in productType.variantAttributes; partial coverage allowed |
| CONF-07 | No two variants within the same product may share the same id |
Section 4 — Objects
| Rule | Description |
|---|---|
| CONF-09 | objectTypeId must reference a defined object type |
| CONF-10 | Object attribute ids must be declared in the resolved objectType.attributeDefinitions |
| CONF-11 | No two objects within the same type group may share the same id |
Section 5 — Assets
| Rule | Description |
|---|---|
| CONF-12 | assetTypeId must reference a defined asset type |
| CONF-13 | Asset attribute ids must be declared in the resolved assetType.attributeDefinitions |
| CONF-14 | No two assets within the same type group may share the same id |
Section 6 — Attribute value types
| Rule | Description |
|---|---|
| CONF-15 | Attributes with localizable: true must have value as a locale-keyed object |
| CONF-16 | Attributes with localizable: false must have value as a direct primitive |
| CONF-17 | Every locale key in any locale-keyed object (attribute values and all labels) must be in model.locales |
| CONF-18 | boolean values must be a JSON boolean — not the string "true" |
| CONF-19 | number values must be a JSON number |
| CONF-20 | text, textarea, and datetime values must be a string |
| CONF-21 | text-list values must be an array of strings |
| CONF-22 | select values must match a selectValue id; localizable select allows different ids per locale |
| CONF-23 | select-list values must be an array of valid selectValue ids |
| CONF-24 | object-link values must reference an existing object id in the correct type group |
| CONF-25 | object-link-list values must be an array of existing object ids in the correct type group |
| CONF-26 | asset-link values must reference an existing asset id in the correct type group |
| CONF-27 | asset-link-list values must be an array of existing asset ids in the correct type group |
| CONF-28 | product-link values must reference an existing product or variant id (snapshot mode only) |
| CONF-29 | product-link-list values must be an array of existing product or variant ids (snapshot mode only) |
| CONF-30 | json attributes accept any valid JSON value — no further enforcement |
Section 7 — Categories
| Rule | Description |
|---|---|
| CONF-44 | categoryTreeId values must be unique across all groups in data.categories |
| CONF-45 | A category node with attribute keys must belong to a group that declares a categoryTypeId |
| CONF-46 | Category node ids must be unique across the entire tree at all depths |
| CONF-47 | Category node attribute ids must be declared in the resolved categoryType.attributeDefinitions |
| CONF-49 | categoryTypeId on a category group must reference a defined categoryType |
| CONF-50 | categoryType ids must be unique within model.categoryTypes |
Section 8 — Model internal integrity
| Rule | Description |
|---|---|
| CONF-31 | productType.productAttributes ids must exist in productAttributeDefinitions |
| CONF-32 | productType.variantAttributes ids must exist in productAttributeDefinitions |
| CONF-33 | Attribute definition ids within each objectType must be unique |
| CONF-34 | Attribute definition ids within each assetType must be unique |
| CONF-35 | selectValue ids within each attribute must be unique |
| CONF-36 | attributeDefinition.objectTypeId must reference a defined objectType |
| CONF-37 | attributeDefinition.assetTypeId must reference a defined assetType |
| CONF-39 | channel.locales must be a subset of model.locales |
| CONF-40 | model.defaultLocale must be present in model.locales |
| CONF-41 | ids within each model-root array must be unique within that array |
| CONF-43 | attributeDefinition.groupId must reference a defined attributeGroup in the correct scope |
Section 9 — Entity id format
| Rule | Description |
|---|---|
| CONF-51 | All ids must match ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ (also enforced by JSON Schema) |
Section 10 — Cross-file references
| Rule | Description |
|---|---|
| CONF-38 | channel.categoryTreeIds must reference trees present in data.categories (snapshot mode only) |
Section 11 — Lifecycle and tombstones
| Rule | Description |
|---|---|
| CONF-52 | lifecycle is required on every product, variant, object, and asset, and must be one of draft, active, archived, deleted (also enforced by the schema and the non-nullable POCO property) |
| CONF-53 | A reference to a deleted entity is valid and must not be flagged as a broken reference — the tombstone is carried in the payload so links resolve |
| CONF-54 | A deleted tombstone is the only conformant delete signal; absence of an entity does not imply deletion |
Section 12 — Markets and market scoping
| Rule | Description |
|---|---|
| CONF-55 | market.locales must be a subset of model.locales |
| CONF-56 | market ids must be unique within model.markets |
| CONF-57 | Every activeForMarkets id (on attribute definitions, select values, products, variants, objects, and assets) must reference a defined market — enforced in both snapshot and incremental mode |
| CONF-58 | Market scoping resolves per market then projects to locales; absent or empty activeForMarkets means all markets (narrow-only, no "scoped to nothing" state). Resolution semantics — not a producer-side violation |
Lifecycle
Every product, variant, object, and asset carries a mandatory Lifecycle (OPXF 0.2):
Lifecycle.Draft // "draft" — not yet published
Lifecycle.Active // "active" — live and current
Lifecycle.Archived // "archived" — retired but retained for reference
Lifecycle.Deleted // "deleted" — tombstone: remove downstream (still carried so links resolve)
Assets additionally carry a required UrlUpdatedAt (DateTimeOffset) whenever a Url is present, for cache-busting and change detection.
Attribute types
AttributeType is an enum of all OPXF canonical types. Each member serializes to its kebab-case
wire string via the built-in converter:
AttributeType.Text // "text"
AttributeType.TextList // "text-list"
AttributeType.Textarea // "textarea"
AttributeType.Number // "number"
AttributeType.Boolean // "boolean"
AttributeType.Datetime // "datetime"
AttributeType.Select // "select"
AttributeType.SelectList // "select-list"
AttributeType.AssetLink // "asset-link"
AttributeType.AssetLinkList // "asset-link-list"
AttributeType.ObjectLink // "object-link"
AttributeType.ObjectLinkList // "object-link-list"
AttributeType.ProductLink // "product-link"
AttributeType.ProductLinkList // "product-link-list"
AttributeType.Json // "json"
Related repositories
| Repository | Description |
|---|---|
| opxf/opxf-spec | The OPXF specification — JSON schemas and conformance rules |
| opxf/opxf-dotnet-akeneo | Akeneo → OPXF exporter built on this library |
| opxf/opxf-org | Source for opxf.org |
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Text.Json (>= 8.0.5)
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.