Repository navigation
fix: make JSON wire format a project setting, defaulting to each language's standard - #457
Merged
Merged
Conversation
…nguage's standard (#454) 1.1.0 normalized every port's generated API onto one wire form with no opt-out: decimals as canonical strings and value-object null members omitted. That broke live C#, Java and Kotlin clients on a 1.x upgrade. The output form is now `wire` in .metaobjects/config.json, read by every port's codegen the way `naming` is (ADR-0019 Amendment 2). Both keys default to native, the language's standard serializer output: C#/Java/Kotlin decimals are JSON numbers again and null members are written; Python and TypeScript keep the decimal's scale. `{"decimals": "string", "nullMembers": "omit"}` restores the 1.1.0 form, and the conformance corpora pin it in their own config. Input rules are unchanged in every mode. Every port's codegen drift gate names the wire block that reproduces committed output generated under another setting. Also fixes the C# DecimalWire.g.cs / MetaNetValidation.g.cs collision when a model binds more than one namespace. Fixes #454
…n wire-format skill
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #454
What this changes
1.1.0 put every port's generated API on one JSON form, with no way back. Decimals became canonical strings (
12.50was sent as"12.5"). C#, Java and Kotlin stopped writing a value object's null members. A C# adopter moving from 1.0.4 to 1.1.1 had to strip attributes by regex and deleteDecimalWire.g.csto get their API back.The output form is now a project setting,
wirein.metaobjects/config.json. Every port's codegen reads it the way it readsnaming, so there is no CLI flag and no generator argument (ADR-0019 Amendment 2):{ "schema_version": 1, "wire": { "decimals": "native", "nullMembers": "native" } }native: what that language's standard serializer writes.{ "decimals": "string", "nullMembers": "omit" }. The conformance corpora pin it in their own config. They never decide a default.@scale/@precisionis400 validation. All five ports keep the generated input converters.meta verify --codegen,dotnet meta verify --codegen,mvn metaobjects:verify,metaobjects verify) says whichwireblock reproduces committed output generated under another setting. It no longer says "regenerate", because regenerating would change the API's JSON.Also fixed: in C#, when the model's packages bind to more than one namespace,
DecimalWire.g.csandMetaNetValidation.g.csare now named per namespace (DecimalWire.<Namespace>.g.cs). Before,genrefused the run with a duplicate output path.The grid (each cell checked against real serializer output from generated code)
decimals: native(default)decimals: stringnullMembers: native(default)emitomit12.50,0.0000001)"12.5"null, key presentBigDecimal(12.50,1E-7)"12.5"null, key present"12.5"null, key presentDecimal("12.50","1E-7")"12.5"nullwhen unset"12.50", not"12.5")"12.50"; 15 significant digits on SQLite and D1)"12.5"nullwhen unset"12.50", not"12.5")There is deliberately no
"number"value. In C#, Java and Kotlin,nativealready is a number. In Python and TypeScript, a number would be a lossy spelling that neither language uses.Two decisions the maintainer made
"12.50", where 1.1.1 wrote"12.5". That is what their standard serializers write. Projects that want the 1.1.x string declare"decimals": "string".Decimalinto a float (12.5, losing digits past a double's precision). Native is now Pydantic'sstr(Decimal). No setting restores the float, because it loses digits. The upgrade guide says so.Docs and skills
docs/features/api-contract.mdhas a new section, "The wire setting", with the full grid.docs/ports/{csharp,java,kotlin,python,typescript}.mdare updated.metaobjects-codegen/references/wire-format.md.DecimalWire.g.cs, but to setwireand regenerate.[1.1.2]section, under Fixed, has an entry for each fix.fixtures/wire-conformance/, which every port runs.Tests
The full
scripts/ci-local.shpassed locally. I also ran these locally:On the pipeline head, the Python full suite including Testcontainers integration passed 4161, and C# Conformance, Codegen and CLI passed 2078, 1007 and 309.
The no-mistakes gate
The gate took one run. Review found four issues, and the pipeline fixed all four in one commit:
FromDirectorynow applieswire.Test passed with an approved exception. Its live verdict was inconclusive (TypeScript was driven live; C#, Java, Kotlin and Python were not), and the maintainer's supervisor approved it on the suite results above. The 1.1.2 release reruns the full
ci-localon main before publishing.No version bump; this does not cut a release.
Validation
Risk Assessment
✅ Low: The four round-1 fixes are correct and covered by behavioural tests. I re-read only the fix-round commit, not the full branch diff.
Testing
Drove the TypeScript
meta genCLI live against disposable projects in /tmp/wire-e2e. An invalid wire value is refused by name with exit 2. Portable config (decimals: string,nullMembers: omit) emits decimalWire/nullMembers options on the generated Order routes. Default config emits none. The runtime wire mapper, given those generated options, behaves as specified: null VO member omitted, decimal canonicalized, native keeps "12.50" and the null member. TS wire-conformance test passes (18). Python wire conformance and m2m shape tests pass (15), but only as unit/conformance runs, not against a live app. Python and TS live generation of the M:N target-shape fix and the C# FromDirectory fix were not exercised live. Full regression suite not run here.Evidence: TS generated portable routes (Order.routes.ts)
Evidence: runtime wire mapper output
Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 4 issues found → auto-fixed ✅
server/python/src/metaobjects/codegen/generators/router_generator.py:880- Python M:N traversal routes write the TARGET's rows through the SOURCE entity's_WIREconstant, sonullMembers: emit|omitdoes not apply to the target's value-object columns._wire_lines([entity], wire)(line 1538) buildsshapesfrom the source entity's fields only._emit_m2m_route(line 880) and_emit_tph_m2m_route(line 937) still returnrender_wire(repo.find_related_<rel>(...), _WIRE). Concrete case: Post M:N Tag, Tag has a jsonbfield.object metaof value objectTagMeta {label, note}, project sets"wire": {"nullMembers": "omit"}. GET /posts/1/tags writes"meta": {"label": "x", "note": null}; the TS port omitsnotebecause its m2m mount getsdecimalColumnsLine(entry.target, ...). Underemit, a stored member that is missing stays absent. When the source and target both declare a value-object field with the same name but different shapes, the target value is shaped with the source's members (emit adds wrong keys). Decimals are not affected, becauserender_wireconverts everyDecimalby type. Fix: emit oneWireOutputper relation built fromobject_index[d.target_entity](plus its TPH subtypes when relevant), and pass that to the m2mrender_wirecalls in both emitters.server/csharp/MetaObjects/Config/MetadataLocation.cs:46- C#MetaDataLoader.FromDirectoryapplies the project'snamingfrom.metaobjects/config.json(MetaObjects/Loader/MetaDataLoader.cs:158, added by fix(csharp): scope generators with Include and apply project naming on directory load #456 for hosts that compose their own registry) but not the newwiresetting. The sibling ports do: JavafromDirectory/fromUriscallsetWireFormat(discoverWireFormat(...))(MetaDataLoader.java:727, 815), and Python_apply_discovered_namingcallsattach_wire(meta_data_loader.py). OnlyResolvedMetadata.Loadand the CLI setRoot.WireFormat. Concrete case: a C# host doesMetaDataLoader.FromDirectory(dir, registry, strict: true)and runsEntityGeneratorwithwire: {decimals: string, nullMembers: omit}declared. It silently emits[DecimalWire.Number]and noJsonIgnore, which is the native output, while that same load honoursnaming. Fix: setresult.Root.WireFormat = cfg.Wirebeside thePhysicalNamingassignment at MetaDataLoader.cs:158 (one line).server/python/src/metaobjects/codegen/runtime/decimal_wire.py:313-_shape_membersrecurses into EVERY non-shape dict value of a row (else _shape_members(v, wire)), so a nested key that happens to equal a value-object column name gets shaped. Concrete case: an entity with value-object columnaddressand an untyped jsonb/map columnextrasholding{"address": {...}}. Underemit,extras.addressgains null-filled Address members. TSnullMembersWirerewrites only the top-level columns it was given.render_wireis always called on rows or a list of rows, never on the envelope, so the nested recursion is not needed. Fix: leave non-shape values untouched (else v).agent-context/skills/metaobjects-codegen/references/wire-format.md:79- The upgrade bullet says that from 1.0.x, "In Python and TypeScript, a decimal keeps its scale (&test(conformance): combined multi-package extends + overlay + cross-package references fixture #34;12.50&test(conformance): combined multi-package extends + overlay + cross-package references fixture #34;)". This implies Python already wrote a string. Python 1.0.x wrote a lossy float (12.5), which the CHANGELOG and upgrade guide state plainly (maintainer decision 2). An agent reading only the skill would tell a Python 1.0.x adopter that nothing changes for clients. Fix: add the Python float-to-string note to this bullet. Not fixed inline because the text is mirrored byte-for-byte into the five fixtures/agent-context-conformance expected copies, which must be updated together.🔧 Fix applied.
✅ Re-checked - no issues remain.
scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsnode server/typescript/packages/cli/dist/bin/meta.js init (in /tmp/wire-e2e/ts)meta gen --dry-run with wire.decimals=canonical -> refused, exit 2meta gen with wire {decimals:string, nullMembers:omit} -> src/generated/Order.routes.ts carries decimalWire/nullMembersmeta gen with no wire block -> routes carry no wire options (native)meta gen with wire {nullMembers:skip} -> refused, exit 2bun /tmp/wire-e2e/drive/ts-wire.ts (withWire from runtime-ts with generated options)PYTHONPATH=server/python/src python3 -m pytest tests/conformance/test_wire_conformance.py + wire/m2m tests -> 15 passed (unit/conformance, not live)bun test packages/sdk/test/wire-conformance.test.ts (server/typescript) -> 18 pass✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.