Summary
The iOS Edit Profile screen can fail while saving unrelated preferences such as units, time format, or date format when gender is unset or encoded with an unsupported value. The app then exposes the raw server-side enum validation message directly to the user:
Invalid option: expected one of "MALE"|"FEMALE"|"OTHER"
This is a profile-contract mismatch rather than a meaningful user error: the user may not be editing gender at all.
Affected environment
Observed with the current public TestFlight client connected to a self-hosted HealthLog server. The screenshot was captured on the native Edit Profile screen while the display-preference controls were visible.
The server-side behavior is present in current main and in the deployed v1.32.x line.
Confirmed server behavior
Runtime profile validation in src/lib/validations/auth.ts accepts only:
gender: z.enum(["MALE", "FEMALE", "OTHER"]).nullable().optional()
Therefore:
- omitted
gender is valid;
gender: null is valid;
gender: "", differently cased values, or other client placeholders return HTTP 422 with the raw Zod enum message.
Both the web and native profile routes pass through applyProfileUpdate() in src/lib/auth/profile-update.ts, so any full-form payload carrying an invalid gender prevents all sibling changes from being persisted.
Contract inconsistencies
Empty-string clearing contract
The profile API documentation says an explicit null or empty string clears a field:
Partial profile update. Every field is optional; an omitted field is left untouched, an explicit null (or empty string) clears it.
Gender does not implement the empty-string-to-null normalization used by several other profile fields.
OpenAPI enum drift
src/lib/openapi/routes/profile.ts currently documents profile gender as:
z.enum(["MALE", "FEMALE"])
for request and response schemas, while the runtime and database also support OTHER.
This can cause a generated or manually implemented native client to use a contract that differs from the actual runtime.
Reproduction
- Use an account whose gender is unset.
- Open More → Edit Profile in the iOS app.
- Change an unrelated setting such as:
- Metric/Imperial display;
- blood-glucose display unit;
- time format;
- date format.
- Save or allow the form to submit.
Actual behavior
The update fails and a red error card shows:
Invalid option: expected one of "MALE"|"FEMALE"|"OTHER"
The unrelated preference may not be saved.
Expected behavior
- An unset gender should be omitted or sent as
null.
- Updating an unrelated preference should not submit or validate untouched profile fields.
- The user should never see a raw validation-library enum message.
Likely native-side cause
The iOS form appears to submit a full profile payload and represent an unset gender with an empty string, lowercase/display value, or another placeholder instead of omitting the field or using null.
The exact native payload should be confirmed in healthlog-iOS, but the server contract defects above are independently reproducible.
Proposed fix
Server
- Normalize
gender: "" to null, matching the documented profile-clearing contract.
- Continue rejecting non-empty unsupported gender values.
- Update all OpenAPI profile request/response schemas to include
OTHER.
- Return a stable field-aware validation envelope instead of exposing raw Zod prose where possible.
iOS
- Omit untouched profile fields from partial updates.
- Encode an unset gender as
null, never "" or a display string.
- Map all supported values exactly to
MALE, FEMALE, and OTHER.
- Show a user-facing field-specific message if validation genuinely fails.
Regression tests
Server
PATCH /api/user/profile with { timeFormat: "H24", gender: "" } normalizes gender to null or ignores it and successfully persists the time format.
gender: null, MALE, FEMALE, and OTHER are accepted.
- Unsupported non-empty values remain rejected.
- OpenAPI generation includes
OTHER in request and response schemas.
iOS
- Saving units/time/date preferences with no gender selected succeeds.
- A preference-only update does not include unrelated profile fields.
OTHER round-trips correctly.
- Server validation errors are presented as friendly field-level messages, not raw Zod output.
Scope
This is separate from the display-preference implementation itself. The profile update is being blocked by an unrelated stale/unset field in the submitted payload.
Summary
The iOS Edit Profile screen can fail while saving unrelated preferences such as units, time format, or date format when gender is unset or encoded with an unsupported value. The app then exposes the raw server-side enum validation message directly to the user:
This is a profile-contract mismatch rather than a meaningful user error: the user may not be editing gender at all.
Affected environment
Observed with the current public TestFlight client connected to a self-hosted HealthLog server. The screenshot was captured on the native Edit Profile screen while the display-preference controls were visible.
The server-side behavior is present in current
mainand in the deployed v1.32.x line.Confirmed server behavior
Runtime profile validation in
src/lib/validations/auth.tsaccepts only:Therefore:
genderis valid;gender: nullis valid;gender: "", differently cased values, or other client placeholders return HTTP 422 with the raw Zod enum message.Both the web and native profile routes pass through
applyProfileUpdate()insrc/lib/auth/profile-update.ts, so any full-form payload carrying an invalid gender prevents all sibling changes from being persisted.Contract inconsistencies
Empty-string clearing contract
The profile API documentation says an explicit
nullor empty string clears a field:Gender does not implement the empty-string-to-null normalization used by several other profile fields.
OpenAPI enum drift
src/lib/openapi/routes/profile.tscurrently documents profile gender as:for request and response schemas, while the runtime and database also support
OTHER.This can cause a generated or manually implemented native client to use a contract that differs from the actual runtime.
Reproduction
Actual behavior
The update fails and a red error card shows:
The unrelated preference may not be saved.
Expected behavior
null.Likely native-side cause
The iOS form appears to submit a full profile payload and represent an unset gender with an empty string, lowercase/display value, or another placeholder instead of omitting the field or using
null.The exact native payload should be confirmed in
healthlog-iOS, but the server contract defects above are independently reproducible.Proposed fix
Server
gender: ""tonull, matching the documented profile-clearing contract.OTHER.iOS
null, never""or a display string.MALE,FEMALE, andOTHER.Regression tests
Server
PATCH /api/user/profilewith{ timeFormat: "H24", gender: "" }normalizes gender to null or ignores it and successfully persists the time format.gender: null,MALE,FEMALE, andOTHERare accepted.OTHERin request and response schemas.iOS
OTHERround-trips correctly.Scope
This is separate from the display-preference implementation itself. The profile update is being blocked by an unrelated stale/unset field in the submitted payload.