You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
RestApiHandler serialization and parsing of compound resource ids are asymmetric for nullable id fields, so a record carries an id in its own JSON:API response that the handler then rejects with HTTP 400 on GET /resource/{thatId}.
Serialize (makeCompoundId): idFields.map(f => item[f.name]).join(idDivider). A null segment becomes '', so a record whose second id field (Int?) is null is emitted with id "200-01|".
Parse (makeIdFilter → coerce): for an Int/Float field, parseInt('') → NaN → InvalidValueError → 400.
Net effect: any model with a nullable field in its external/compound id has records that are not addressable by the very id the API returns for them.
Steps to reproduce:
Model with @@unique([a, b]) where b is nullable (Int?), exposed via REST with that pair as the external id (externalIdMapping).
Create a record with b = null.
GET /things → that record's id comes back as "<a>|".
GET /things/<a>| → 400 (invalid Int value: ).
Expected: the round-trip succeeds — an empty segment for an optional field parses as SQL NULL, and the record is returned (findUnique matches the null row; verified that the ORM lowers null to IS NULL). At minimum, the id the handler emits must be one it can accept back.
Suggested fix: make coerce symmetric with makeCompoundId (empty segment + optional field → null), or expose a public/overridable coerce (or per-field id-encoding hook) so consumers can fix it without patching a private method. Note: "" vs. a genuine empty-string segment is ambiguous for String id fields; it is unambiguous for typed-nullable fields (Int?/Float?), the common case.
Screenshots
N/A.
Environment (please complete the following information):
ZenStack version: 3.7.0
Database type: PostgreSQL
Node.js/Bun version: 24.15.0
Package manager: npm 11.12.1
Additional context
Related: Invalid compound IDs lead to 400, should be 404 #2211 (Invalid compound IDs lead to 400, should be 404, open) — same makeIdFilter/coerce path, but a different case: a missing segment (/user/a for an a_b key) that should 404. This issue is a present-but-empty segment denoting null that should resolve 200. A fix should cover both; the desired outcomes differ.
Description and expected behavior
RestApiHandlerserialization and parsing of compound resource ids are asymmetric for nullable id fields, so a record carries anidin its own JSON:API response that the handler then rejects with HTTP 400 onGET /resource/{thatId}.makeCompoundId):idFields.map(f => item[f.name]).join(idDivider). Anullsegment becomes'', so a record whose second id field (Int?) is null is emitted with id"200-01|".makeIdFilter→coerce): for anInt/Floatfield,parseInt('')→NaN→InvalidValueError→ 400.Net effect: any model with a nullable field in its external/compound id has records that are not addressable by the very id the API returns for them.
Steps to reproduce:
@@unique([a, b])wherebis nullable (Int?), exposed via REST with that pair as the external id (externalIdMapping).b = null.GET /things→ that record'sidcomes back as"<a>|".GET /things/<a>|→ 400 (invalid Int value:).Expected: the round-trip succeeds — an empty segment for an optional field parses as SQL
NULL, and the record is returned (findUniquematches the null row; verified that the ORM lowersnulltoIS NULL). At minimum, the id the handler emits must be one it can accept back.Suggested fix: make
coercesymmetric withmakeCompoundId(empty segment + optional field →null), or expose a public/overridablecoerce(or per-field id-encoding hook) so consumers can fix it without patching a private method. Note:""vs. a genuine empty-string segment is ambiguous forStringid fields; it is unambiguous for typed-nullable fields (Int?/Float?), the common case.Screenshots
N/A.
Environment (please complete the following information):
Additional context
makeIdFilter/coercepath, but a different case: a missing segment (/user/afor ana_bkey) that should 404. This issue is a present-but-empty segment denotingnullthat should resolve 200. A fix should cover both; the desired outcomes differ.externalIdMappingfeature this surfaces under.coerceto map empty-segment + optional-field →null. To be removed once fixed upstream.