Skip to content

[Bug] iOS profile updates fail on unset gender and expose raw enum validation error #645

Description

@lutzkind

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

  1. Use an account whose gender is unset.
  2. Open More → Edit Profile in the iOS app.
  3. Change an unrelated setting such as:
    • Metric/Imperial display;
    • blood-glucose display unit;
    • time format;
    • date format.
  4. 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

  1. Normalize gender: "" to null, matching the documented profile-clearing contract.
  2. Continue rejecting non-empty unsupported gender values.
  3. Update all OpenAPI profile request/response schemas to include OTHER.
  4. Return a stable field-aware validation envelope instead of exposing raw Zod prose where possible.

iOS

  1. Omit untouched profile fields from partial updates.
  2. Encode an unset gender as null, never "" or a display string.
  3. Map all supported values exactly to MALE, FEMALE, and OTHER.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions