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
Follow-up to #375. While fixing the TIMESTAMP write/read defect, we made the response→domain conversion fail loud when an explicit CloudKit type contradicts the decoded value's shape, instead of silently coercing.
That strict validation currently covers scalartype tags only. This issue tracks extending it to complex/list declared types.
Scalar errors thrown today (the current behavior)
The valueoneOf in the generated FieldValueResponse is undiscriminated — the decoder is first-match-wins (String → Int64 → Double → Bytes → Date). Response conversion (Sources/MistKit/Models/FieldValues/FieldValue+Components+Scalar.swift) therefore honors an explicit type over the decoded case, and validates each scalar type against the value's category:
Numeric scalar tags — TIMESTAMP, DOUBLE, INT64 — require the value to be a number (numericValue(from:) non-nil, i.e. decoded as Int64Value/DoubleValue/DateValue). Validated by requireNumeric(_:fieldName:declaredType:).
String scalar tags — STRING, BYTES — require the value to be a string (stringValue(from:) non-nil, i.e. StringValue/BytesValue). Validated by requireString(_:fieldName:declaredType:).
When the category doesn't match, conversion throws:
// Sources/MistKit/Models/ConversionError.swift
case typeValueMismatch(fieldName: String, declaredType: String, value: String)
Message format:
Field '<fieldName>' declared type <DECLARED_TYPE> but its value is incompatible (<value>)
declaredType is the literal CloudKit tag string ("TIMESTAMP", "DOUBLE", "INT64", "BYTES", "STRING"). It is thrown via reportAndThrow(), so in DEBUG it trips the ConversionFailureReporter assertion handler first (suppressible in tests via ConversionFailureReporter.$assertionHandler.withValue), and in release it logs to the .api subsystem then throws the typed ConversionError.
Truth table (scalar tags)
Response
Result
TIMESTAMP + "hello"
throwstypeValueMismatch
BYTES + 42
throws
STRING + 42
throws
DOUBLE + "x"
throws
INT64 + "x"
throws
TIMESTAMP + {recordName:…}
throws
INT64 + 3.5
.double(3.5) — numeric category OK, defers to inference (no truncation)
STRING + "hello"
.string("hello")
The gap
The strict check is scoped to scalar tags. Complex/list declared types are not validated against the value — they fall through makeTypedScalar's default: to inference / makeComplexFieldValue, deferring entirely to the value's self-describing structure. So a contradiction like:
Response
Result today
REFERENCE + 42
.int64(42) (tag ignored, no throw)
ASSET + "text"
.string("text") (no throw)
LIST + 42
.int64(42) (no throw)
This is the documented, deliberate boundary from #375 (see the "Response type recovery" note in CLAUDE.md), chosen to limit read-path regression risk. But it's asymmetric: TIMESTAMP over a non-number throws, while REFERENCE over a non-object is silently coerced.
Proposed work
Decide whether complex/list type tags should be validated against the decoded value the same way scalars are (numeric/string contradiction → typeValueMismatch).
If yes, map each complex declared type to its acceptable decoded oneOf case(s):
REFERENCE → ReferenceValue
ASSET / ASSETID → AssetValue (decide how ASSETID maps; today there's no distinct domain case)
LOCATION → LocationValue
LIST → ListValue (and consider element-type tags, e.g. *_LIST)
throw typeValueMismatch (or a new dedicated case) when the value's structure can't satisfy the tag.
Watch for read-path robustness regressions — complex value mapping is looser than scalars (e.g. asset-vs-location oneOf greediness, see the assetFieldValueConvertsToAsset regression test), so validate this doesn't reject well-formed responses.
Add tests mirroring FieldValueConversionTests+ResponseTypes.swift (complexTypeContradictionStaysLenient currently asserts the lenient behavior and would flip).
Pointers
Conversion: Sources/MistKit/Models/FieldValues/FieldValue+Components+Scalar.swift (makeTypedScalar, requireNumeric, requireString) and FieldValue+Components.swift (makeComplexFieldValue)
Background
Follow-up to #375. While fixing the
TIMESTAMPwrite/read defect, we made the response→domain conversion fail loud when an explicit CloudKittypecontradicts the decoded value's shape, instead of silently coercing.That strict validation currently covers scalar
typetags only. This issue tracks extending it to complex/list declared types.Scalar errors thrown today (the current behavior)
The
valueoneOfin the generatedFieldValueResponseis undiscriminated — the decoder is first-match-wins (String → Int64 → Double → Bytes → Date). Response conversion (Sources/MistKit/Models/FieldValues/FieldValue+Components+Scalar.swift) therefore honors an explicittypeover the decoded case, and validates each scalar type against the value's category:TIMESTAMP,DOUBLE,INT64— require the value to be a number (numericValue(from:)non-nil, i.e. decoded asInt64Value/DoubleValue/DateValue). Validated byrequireNumeric(_:fieldName:declaredType:).STRING,BYTES— require the value to be a string (stringValue(from:)non-nil, i.e.StringValue/BytesValue). Validated byrequireString(_:fieldName:declaredType:).When the category doesn't match, conversion throws:
Message format:
declaredTypeis the literal CloudKit tag string ("TIMESTAMP","DOUBLE","INT64","BYTES","STRING"). It is thrown viareportAndThrow(), so in DEBUG it trips theConversionFailureReporterassertion handler first (suppressible in tests viaConversionFailureReporter.$assertionHandler.withValue), and in release it logs to the.apisubsystem then throws the typedConversionError.Truth table (scalar tags)
TIMESTAMP+"hello"typeValueMismatchBYTES+42STRING+42DOUBLE+"x"INT64+"x"TIMESTAMP+{recordName:…}INT64+3.5.double(3.5)— numeric category OK, defers to inference (no truncation)STRING+"hello".string("hello")The gap
The strict check is scoped to scalar tags. Complex/list declared types are not validated against the value — they fall through
makeTypedScalar'sdefault:to inference /makeComplexFieldValue, deferring entirely to the value's self-describing structure. So a contradiction like:REFERENCE+42.int64(42)(tag ignored, no throw)ASSET+"text".string("text")(no throw)LIST+42.int64(42)(no throw)This is the documented, deliberate boundary from #375 (see the "Response type recovery" note in
CLAUDE.md), chosen to limit read-path regression risk. But it's asymmetric:TIMESTAMPover a non-number throws, whileREFERENCEover a non-object is silently coerced.Proposed work
typetags should be validated against the decoded value the same way scalars are (numeric/string contradiction →typeValueMismatch).oneOfcase(s):REFERENCE→ReferenceValueASSET/ASSETID→AssetValue(decide howASSETIDmaps; today there's no distinct domain case)LOCATION→LocationValueLIST→ListValue(and consider element-type tags, e.g.*_LIST)typeValueMismatch(or a new dedicated case) when the value's structure can't satisfy the tag.assetFieldValueConvertsToAssetregression test), so validate this doesn't reject well-formed responses.FieldValueConversionTests+ResponseTypes.swift(complexTypeContradictionStaysLenientcurrently asserts the lenient behavior and would flip).Pointers
Sources/MistKit/Models/FieldValues/FieldValue+Components+Scalar.swift(makeTypedScalar,requireNumeric,requireString) andFieldValue+Components.swift(makeComplexFieldValue)Sources/MistKit/Models/ConversionError.swift(typeValueMismatch)Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+ResponseTypes.swiftCLAUDE.md→ "Response type recovery (issue FieldValueRequest can't tag scalar TIMESTAMP — Date/Time writes fail with BAD_REQUEST #375)"