Repository navigation
1.1 changed the C# decimal and VO-null wire format by default with no opt-out; make it a setting and emit null for VO members #454
Description
⚠️ Written by Claude (AI) on behalf of Doug Mealing. It may contain errors — take it with a grain of salt and verify before acting.
Summary
1.1.0 changed two parts of the C# wire contract by default, with no opt-out:
- Decimals are now JSON strings (ADR-0019 Amendment 1, W1). Every
field.decimalproperty gets a[JsonConverter(typeof(DecimalWire.…))]or[DecimalWire.Declared(p, s)]attribute, plus aDecimalWire.g.csper namespace. - Value-object members that are null are now left out of the JSON (CHANGELOG
[1.1.0]: "C# and Java: a value object omits an unset member on the wire instead of sendingnull, as the other ports do"). Every nullable VO member gets[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)].
Both changes come from normalizing the ports, which was the goal. But the existing C# adopters are the ones whose wire changed. A brownfield C# adopter that upgrades from 1.0.4 to 1.1.1 changes the JSON its live clients read (12.5 becomes "12.5", and {"x": null} becomes {}) without touching a line of metadata. Nothing fails at build or test time. The only way to keep the old contract today is to post-process the generated text: strip the attributes with a regex and drop DecimalWire.g.cs. That is what we had to do, and it is brittle.
Ask 1: make the wire format a project setting, like naming
naming in 1.1 is the right pattern. The default changed, one key in .metaobjects/config.json keeps the old behavior, every port reads it, and verify names the setting on a mismatch. The wire format should work the same way, for example:
{
"schema_version": 1,
"wire": {
"decimals": "string",
"nullMembers": "emit"
}
}wire.decimals:"string"(the 1.1 default, W1–W4) or"number"(the 1.0 C#/Java/Kotlin behavior). This lets a brownfield project move to strings deliberately, as its own consumer-reviewed change, instead of as a side effect of a version bump.wire.nullMembers: see Ask 2 for the default.
Like naming, this must be implemented in every port. A port that ignores the key would silently disagree, which is the same trap the 1.1 upgrade guide warns about for naming.
Ask 2: emit null for value-object members, in every port (revisit the default)
Leaving nulls out looks like the wrong rule to normalize on:
- It contradicts the decimal rule in the same release. W1 says "Null is
null, with the key present." So entity fields keep the key while VO members drop it: two null rules in one payload. - It loses the absent-versus-null distinction that the generated PATCH contract relies on. The CHANGELOG states the contract: an absent field is untouched, and an explicit
nullclears it. If a client reads a VO with nulls left out and sends it back, it can no longer say "clear". - It breaks clients.
=== nullchecks stop matching, and TypeScript consumers see optional keys (x?: T) instead ofx: T | null. - It changes stored data, not only API responses. A VO persisted as jsonb through the System.Text.Json converter (the generated
ObjectConverter<TModel>) now writes documents without those keys. Key-existence queries (?,jsonb_exists) then behave differently on old and new rows.
Suggested resolution: always write the key with null (T | null) in all five ports, and treat an absent key as "not provided" on input only. That changes TS and Python instead of C# and Java, so it is breaking too, which is another reason it belongs behind Ask 1's setting.
Related bug: DecimalWire.g.cs collides in a single output directory
EntityGenerator.EmitDecimalWireFile always writes the bare path "DecimalWire.g.cs", once per namespace that has a decimal property (EntityGenerator.cs, around line 186 and line 1316). If a project maps objects to several namespaces through TypeOverrides / Convention but writes them all into one OutDir, the copies get the same path and CodegenRunner.Run throws:
System.InvalidOperationException: duplicate generated output path "DecimalWire.g.cs" — emitted by both "entity" and "entity".
MetaNetValidation.g.cs (EmitNetValidationFile) has the same latent problem. Possible fixes: put the namespace in the file name (DecimalWire.<Namespace>.g.cs), or emit one copy in the common ancestor namespace.
Repro / versions
- Packages:
MetaObjects,MetaObjects.Codegen,MetaObjects.Render1.1.1, upgraded from 1.0.4; EF Core 8, PostgreSQL. - Codegen is hosted (
CodegenRunner.Runwith a subclassedEntityGenerator). ASP.NET MVC output uses System.Text.Json (AddJsonOptions), so the per-property attributes apply to every entity- or VO-shaped response.
Activity
dmealing commentedon Oct 24, 2026
PR open: #457 (for 1.1.2).
The JSON a generated API writes is now the project setting
wirein.metaobjects/config.json, read the waynamingis. Both keys default tonative, which is what that language's standard serializer writes:null, as in 1.0.x."wire": { "decimals": "string", "nullMembers": "omit" }.wireblock that reproduces committed output, instead of telling you to regenerate.DecimalWire.g.cscollision across namespaces is fixed.The PR body and
docs/features/api-contract.md("The wire setting") have the per-port grid.chore(release): PyPI, NuGet and Maven Central 1.1.2 / 8.1.2