Unified health-data schema: one shape for four platforms
A vendor-neutral JSON Schema (draft 2020-12) and TypeScript types for 9 common health records — heart rate, steps, sleep sessions with stages, workouts, weight, body fat, height, heart rate variability and SpO2 — with each field mapped, or marked not mapped, for Apple HealthKit, Android Health Connect, the Google Health API and Wear OS Health Services. Each mapping quotes the platform's own documentation, each unit conversion names the sentence that states it (HealthKit's percent is 0.0 to 1.0, Google stores weight in grams), and each gap says why: Wear OS covers 3 of the 9, and HealthKit's SDNN is never relabeled as RMSSD. Version 1.0.0, CC BY 4.0.
Version 1.0.0. $id: https://aifitnessapi.com/schema/v1/unified-health-data.schema.json. Licensed CC BY 4.0, like the open datasets: attribute aifitnessapi.com. A later version gets a new path; v1 stays where it is.
How to read the tables
- Every record has a
typeand asource(platform,nativeType, optionalsourceName), then its time fields and its values. Instants are RFC 3339 in UTC; offsets are ±HH:MM or null, never guessed. - A mapped cell names the platform field, its declared type, any conversion, and a Source with the platform’s own sentence. A Not mapped cell says why. Counts: HealthKit 23 mapped / 14 not; Health Connect 35 / 2; Google Health API 37 / 0; Wear OS 4 / 33.
- HealthKit and Health Connect are native APIs with no JSON wire format. Their columns name API properties; the converter reads them in the encodings this site documents (export.xml rows, and the synthetic data generator’s JSON).
Heart rate sample type: "heartRate"
One heart-rate reading at one instant, in beats per minute. Shape: one instant.
| Unified field | HealthKit HKQuantityTypeIdentifier.heartRate | Health Connect HeartRateRecord | Google Health API heart-rate | Wear OS Health Services DataType.HEART_RATE_BPM |
|---|---|---|---|---|
timestring | startDateDate a sample at one moment has startDate equal to endDate | heartRate.sampleTime.physicalTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | |
utcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | startZoneOffsetZoneOffset? the record's startZoneOffset, applied to every sample (a Sample has no offset of its own) SourceUser experienced zone offset at startTime, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | heartRate.sampleTime.utcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time during the observation relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
beatsPerMinutenumber · count/min | quantityHKQuantity (count/time unit family) read with doubleValue(for:) in count/min SourceThese samples use count/time units (described in ) and measure discrete values (described in ).developer.apple.com | samples[].beatsPerMinute@IntRange (from = 1, to = 300) Long | heartRate.beatsPerMinutestring/int64 int64 arrives as a JSON string: parse to a number SourceRequired. The heart rate value in beats per minute. Must be in the range `[1, 300]`.developers.google.com |
Step count over an interval type: "steps"
The number of steps taken between two instants. Shape: an interval.
| Unified field | HealthKit HKQuantityTypeIdentifier.stepCount | Health Connect StepsRecord | Google Health API steps | Wear OS Health Services DataType.STEPS |
|---|---|---|---|---|
startTimestring | steps.interval.startTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | ||
endTimestring | steps.interval.endTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | ||
startUtcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | startZoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at startTime, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | steps.interval.startUtcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time at the start of the observation relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
endUtcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | endZoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at endTime, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | steps.interval.endUtcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time at the end of the observation relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
countinteger · count | quantityHKQuantity (count unit family) read with doubleValue(for:) in count SourceThese samples use count units (described in ) and measure cumulative values (described in ).developer.apple.com | steps.countstring/int64 int64 arrives as a JSON string: parse to an integer SourceRequired. Number of steps in the recorded interval. Must be in the range `[0, 1000000]`.developers.google.com |
- Google prints different lower bounds: Health Connect's count is “Valid range: 1-1000000.”, the Google Health API's “Required. Number of steps in the recorded interval. Must be in the range `[0, 1000000]`.” A zero-step interval converts to the unified shape, but cannot be written back to Health Connect.
Sleep session with stages type: "sleepSession"
One sleep period and the stages inside it, each stage an interval with one value from the unified stage vocabulary. Shape: a session (an interval with contents).
| Unified field | HealthKit HKCategoryTypeIdentifier.sleepAnalysis | Health Connect SleepSessionRecord | Google Health API sleep | Wear OS Health Services No type |
|---|---|---|---|---|
startTimestring | sleep.interval.startTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | ||
endTimestring | sleep.interval.endTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | ||
startUtcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | startZoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at startTime, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | sleep.interval.startUtcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time at the start of the session relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
endUtcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | endZoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at endTime, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | sleep.interval.endUtcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time at the end of the session relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
stagesarray | value (HKCategoryValueSleepAnalysis)Int each sample's startDate, endDate and value become one stage | stages[].startTime, stages[].endTime, stages[].stageList<SleepSessionRecord.Stage>; stage: Int stage Int → the unified vocabulary (table below) | sleep.stages[] (startTime, endTime, type)array<SleepStage> type string → the unified vocabulary (table below); outOfBedSegments are not read SourceOptional. List of non-overlapping contiguous sleep stage segments that cover the sleep period.developers.google.com | Not mapped No sleep type in Health Services. |
- Stage vocabularies differ: Health Connect separates awake from awake-in-bed and out-of-bed, the Google Health API adds restless, and Apple's core stage is “light or intermediate”. The unified vocabulary keeps every distinction any source makes, so a conversion never merges two source values — except Apple's core into light, stated in the table.
Sleep stages: 10 unified values
Each platform value maps to exactly one unified stage, with the platform’s own description beside it. The unified vocabulary: awake, awakeInBed, outOfBed, inBed, asleep, light, deep, rem, restless, unknown.
| Value | Unified | Platform says |
|---|---|---|
HKCategoryValueSleepAnalysisInBed | inBed | The user is in bed. |
HKCategoryValueSleepAnalysisAwake | awake | The user is awake. |
HKCategoryValueSleepAnalysisAsleepCore | light | The user is in light or intermediate sleep. Apple's core stage is “light or intermediate”; the unified vocabulary has no intermediate stage, so it lands on light. |
HKCategoryValueSleepAnalysisAsleepDeep | deep | The user is in deep sleep. |
HKCategoryValueSleepAnalysisAsleepREM | rem | The user is in REM sleep. |
HKCategoryValueSleepAnalysisAsleepUnspecified | asleep | The user is asleep, but the specific stage isn’t known. |
HKCategoryValueSleepAnalysisAsleep | asleep | The user is sleeping. Deprecated in iOS 16.0; Apple's availability data names HKCategoryValueSleepAnalysis.asleepUnspecified as its new name. |
| Value | Unified | Platform says |
|---|---|---|
STAGE_TYPE_UNKNOWN (0) | unknown | Use this type if the stage of sleep is unknown. |
STAGE_TYPE_AWAKE (1) | awake | The user is awake and either known to be in bed, or it is unknown whether they are in bed or not. |
STAGE_TYPE_SLEEPING (2) | asleep | The user is asleep but the particular stage of sleep (light, deep or REM) is unknown. |
STAGE_TYPE_OUT_OF_BED (3) | outOfBed | The user is out of bed and assumed to be awake. |
STAGE_TYPE_LIGHT (4) | light | The user is in a light sleep stage. |
STAGE_TYPE_DEEP (5) | deep | The user is in a deep sleep stage. |
STAGE_TYPE_REM (6) | rem | The user is in a REM sleep stage. |
STAGE_TYPE_AWAKE_IN_BED (7) | awakeInBed | The user is awake and in bed. |
| Value | Unified | Platform says |
|---|---|---|
SLEEP_STAGE_TYPE_UNSPECIFIED | unknown | The default unset value. |
AWAKE | awake | Sleep stage AWAKE. |
LIGHT | light | Sleep stage LIGHT. |
DEEP | deep | Sleep stage DEEP. |
REM | rem | Sleep stage REM. |
ASLEEP | asleep | Sleep stage ASLEEP. |
RESTLESS | restless | Sleep stage RESTLESS. |
Workout session type: "workout"
One exercise session: when it ran, what it was, and the totals where the source states them. Shape: a session (an interval with contents).
| Unified field | HealthKit HKWorkout | Health Connect ExerciseSessionRecord | Google Health API exercise | Wear OS Health Services ExerciseType |
|---|---|---|---|---|
startTimestring | exercise.interval.startTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | ||
endTimestring | exercise.interval.endTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | ||
startUtcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | startZoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at startTime, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | exercise.interval.startUtcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time at the start of the session relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
endUtcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | endZoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at endTime, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | exercise.interval.endUtcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time at the end of the session relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
activityTypestring | null | workoutActivityTypeHKWorkoutActivityType Objective-C constant → crosswalk key | exerciseTypeInt Int value → EXERCISE_TYPE_ constant → crosswalk key | exercise.exerciseTypestring enum string → crosswalk key | nameString constant name → crosswalk key |
nativeActivityTypestring | workoutActivityTypeHKWorkoutActivityType | exerciseTypeInt | exercise.exerciseTypestring | nameString |
energyKcalnumber | null · kcal · optional | Not mapped HKWorkout.totalEnergyBurned is deprecated as of iOS 18.0 (Apple: “Use statisticsForType: passing the HKQuantityType for HKQuantityTypeIdentifierActiveEnergyBurned”); per-type statistics are not in this site's datasets, so the converter does not read energy from HealthKit. | Not mapped ExerciseSessionRecord has no energy property: Health Connect stores it as separate calories records over the same interval. | exercise.metricsSummary.caloriesKcalnumber/double | Not mapped Exercise totals in Health Services arrive as DataType values during the exercise (CALORIES_TOTAL and similar), not on a session object; not mapped here. |
distanceMetersnumber | null · m · optional | Not mapped HKWorkout.totalDistance is deprecated as of iOS 27.3 (Apple: “Use statisticsForType: passing the HKQuantityType for the desired distance type”); not read. | Not mapped ExerciseSessionRecord has no distance property: Health Connect stores it as separate DistanceRecord records. | exercise.metricsSummary.distanceMillimetersnumber/double ÷ 1,000 (the field name states millimeters) | Not mapped As for energy: DISTANCE_TOTAL-style values arrive during the exercise; not mapped here. |
- activityType uses the workout crosswalk's key (87 keys are shared by two or more platforms). A key shared by name is not a claim that two platforms mean the same activity; /workout-types shows each platform's own description.
Body mass (weight) type: "bodyMass"
The user's weight at one instant, in kilograms. Shape: one instant.
| Unified field | HealthKit HKQuantityTypeIdentifier.bodyMass | Health Connect WeightRecord | Google Health API weight | Wear OS Health Services No type |
|---|---|---|---|---|
timestring | startDateDate a sample at one moment has startDate equal to endDate | weight.sampleTime.physicalTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | |
utcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | zoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at time, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | weight.sampleTime.utcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time during the observation relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
kilogramsnumber · kg | quantityHKQuantity (mass unit family) read with doubleValue(for:) in kg SourceThese samples use mass units (described in ) and measure discrete values (described in ).developer.apple.com | weightMass Mass → kilograms (inKilograms) SourceUser's weight in kilograms. Required field. Valid range: 0-1000 kilograms.developer.android.com | weight.weightGramsnumber/double ÷ 1,000 SourceRetrieve the user's weight in grams from the `weightGrams` field in the Weight data type. Convert this to kilograms (divide by 1,000) or pounds (divide by 453.59237).developers.google.com | Not mapped No body-mass type in Health Services. |
- Pounds: Google's migration guide divides grams by 453.59237 (“Retrieve the user's weight in grams from the `weightGrams` field in the Weight data type. Convert this to kilograms (divide by 1,000) or pounds (divide by 453.59237).”), and elsewhere says “Retrieve the user's weight in grams from the `weightGrams` field in the Weight data type and convert to kilograms (divide by 1,000). To convert pounds to kilograms, divide by 2.2.” The converter uses 453.59237 g per pound.
Body fat percentage type: "bodyFat"
Body fat as a percentage from 0 to 100. Shape: one instant.
| Unified field | HealthKit HKQuantityTypeIdentifier.bodyFatPercentage | Health Connect BodyFatRecord | Google Health API body-fat | Wear OS Health Services No type |
|---|---|---|---|---|
timestring | startDateDate a sample at one moment has startDate equal to endDate | bodyFat.sampleTime.physicalTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | |
utcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | zoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at time, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | bodyFat.sampleTime.utcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time during the observation relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
percentnumber · % | quantityHKQuantity (percent unit family) × 100 (Apple: “Percent measures a value between 0.0 and 1.0.”) | bodyFat.percentagenumber/double | Not mapped No body-fat type in Health Services. |
- HealthKit's percent unit is a fraction (0.0–1.0); Health Connect and the Google Health API state 0–100. Mixing them is the classic 0.23-percent body-fat bug.
Height type: "height"
The user's height at one instant, in meters. Shape: one instant.
| Unified field | HealthKit HKQuantityTypeIdentifier.height | Health Connect HeightRecord | Google Health API height | Wear OS Health Services No type |
|---|---|---|---|---|
timestring | startDateDate a sample at one moment has startDate equal to endDate | height.sampleTime.physicalTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | |
utcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | zoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at time, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | height.sampleTime.utcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time during the observation relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
metersnumber · m | quantityHKQuantity (length unit family) read with doubleValue(for:) in m SourceThese samples use length units (described in ) and measure discrete values (described in ).developer.apple.com | heightLength Length → meters (inMeters) | height.heightMillimetersstring/int64 ÷ 1,000 (int64 arrives as a JSON string) SourceRetrieve the user's height in millimeters from the `heightMillimeters` field in the Height data type. Convert this to meters (divide by 1,000) or inches (divide by 25.4).developers.google.com | Not mapped No height type in Health Services. |
Heart rate variability type: "heartRateVariability"
One HRV reading in milliseconds, tagged with the statistic it is. SDNN and RMSSD are different measures and never convert into each other. Shape: one instant.
| Unified field | HealthKit HKQuantityTypeIdentifier.heartRateVariabilitySDNN | Health Connect HeartRateVariabilityRmssdRecord | Google Health API heart-rate-variability | Wear OS Health Services No type |
|---|---|---|---|---|
timestring | startDateDate a sample at one moment has startDate equal to endDate | heartRateVariability.sampleTime.physicalTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | |
utcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | zoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at time, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | heartRateVariability.sampleTime.utcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time during the observation relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
metricstring | (the type)sdnn always sdnn SourceA quantity sample type that measures the standard deviation of heartbeat intervals.developer.apple.com | (the record class)rmssd always rmssd SourceCaptures user's heart rate variability (HRV) as measured by the root mean square of successive differences (RMSSD) between normal heartbeats.developer.android.com | heartRateVariability.rootMeanSquareOfSuccessiveDifferencesMillisecondsrmssd rmssd when this field is present SourceOptional. The root mean square of successive differences between normal heartbeats. This is a measure of heart rate variability used by Google Health. Must be in the range `[1, 200]`.developers.google.com | Not mapped No HRV type in Health Services. |
millisecondsnumber · ms | quantityHKQuantity (time unit family) read with doubleValue(for:) in ms SourceThese samples use time units (described in ) and measure discrete values (described in ).developer.apple.com | heartRateVariabilityMillisDouble SourceHeart rate variability in milliseconds. Required field. Valid Range: 1-200.developer.android.com | heartRateVariability.rootMeanSquareOfSuccessiveDifferencesMillisecondsnumber/double SourceOptional. The root mean square of successive differences between normal heartbeats. This is a measure of heart rate variability used by Google Health. Must be in the range `[1, 200]`.developers.google.com | Not mapped No HRV type in Health Services. |
- HealthKit publishes SDNN; Health Connect and the Google Health API publish RMSSD. A pipeline that stores both in one column without the metric tag mixes two different numbers.
heartRateVariability.standardDeviationMilliseconds(Google Health API): Not mapped to sdnn. Google describes it as “Optional. The standard deviation of the heart rate variability measurement.” — the deviation of the measurement, not of the heartbeat intervals — so calling it SDNN would be a guess.
Blood oxygen saturation (SpO2) type: "oxygenSaturation"
One SpO2 reading as a percentage from 0 to 100. Shape: one instant.
| Unified field | HealthKit HKQuantityTypeIdentifier.oxygenSaturation | Health Connect OxygenSaturationRecord | Google Health API oxygen-saturation | Wear OS Health Services No type |
|---|---|---|---|---|
timestring | startDateDate a sample at one moment has startDate equal to endDate | oxygenSaturation.sampleTime.physicalTimestring/google-datetime | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. | |
utcOffsetstring | null · optional | Not mapped HealthKit samples carry no UTC offset field. An export.xml date includes the offset (the converter reads it); in the API, the optional HKMetadataKeyTimeZone metadata is a zone name, not an offset. | zoneOffsetZoneOffset? ZoneOffset → ±HH:MM; null stays null SourceUser experienced zone offset at time, or null if unknown. Providing these will help history aggregations results stay consistent should user travel. Queries with user experienced time filters will assume system current zone offset if the information is absent.developer.android.com | oxygenSaturation.sampleTime.utcOffsetstring/google-duration google-duration seconds ("3600s") → ±HH:MM SourceRequired. The offset of the user's local time during the observation relative to the Coordinated Universal Time (UTC).developers.google.com | Not mapped Health Services DataPoint time fields are not in this site's Health Services dataset, and Health Services has no JSON form to convert. |
percentnumber · % | quantityHKQuantity (percent unit family) × 100 (Apple: “Percent measures a value between 0.0 and 1.0.”) | oxygenSaturation.percentagenumber/double SourceRequired. The oxygen saturation percentage. Must be in the range `[0, 100]`.developers.google.com | Not mapped No SpO2 type in Health Services. |
Unit conversions, each with its source
A factor appears only where a source states it, or where the platform’s field name states its unit and the factor is an SI prefix. Every other mapped value is already in the unified unit.
| Record · platform | From | To | Factor and source |
|---|---|---|---|
| Workout session · Google Health API | exercise.metricsSummary.distanceMillimeters | distanceMeters | × 0.001 — ÷ 1,000 (the field name states millimeters)Optional. Total distance covered by the user during the exercise.source |
| Body mass (weight) · Google Health API | weight.weightGrams | kilograms | × 0.001 — ÷ 1,000Retrieve the user's weight in grams from the `weightGrams` field in the Weight data type. Convert this to kilograms (divide by 1,000) or pounds (divide by 453.59237).source |
| Body fat percentage · HealthKit | quantity | percent | × 100 — × 100 (Apple: “Percent measures a value between 0.0 and 1.0.”)Percent measures a value between 0.0 and 1.0.source |
| Height · Google Health API | height.heightMillimeters | meters | × 0.001 — ÷ 1,000 (int64 arrives as a JSON string)Retrieve the user's height in millimeters from the `heightMillimeters` field in the Height data type. Convert this to meters (divide by 1,000) or inches (divide by 25.4).source |
| Blood oxygen saturation (SpO2) · HealthKit | quantity | percent | × 100 — × 100 (Apple: “Percent measures a value between 0.0 and 1.0.”)Percent measures a value between 0.0 and 1.0.source |
Time: Health Connect writes offsets as a ZoneOffset (+HH:MM), the Google Health API as a google-duration in seconds (“7200s” is +02:00), and HealthKit samples carry no offset field at all. The converter also reads pounds and inches by the factors Google’s migration guide prints (453.59237 g, 25.4 mm), and refuses a unit it has no sourced factor for rather than assume one.
What is not mapped, and why
- Sleep session with stages on Wear OS Health Services. None of the 65 Health Services DataType constants (Jetpack reference, read 2026-10-04) has a name or description mentioning “sleep”: no sleep type to map.
- Body mass (weight) on Wear OS Health Services. None of the 65 Health Services DataType constants (Jetpack reference, read 2026-10-04) has a name or description mentioning “weight” or “body mass”: no body-mass type to map.
- Body fat percentage on Wear OS Health Services. None of the 65 Health Services DataType constants (Jetpack reference, read 2026-10-04) has a name or description mentioning “fat”: no body-fat type to map.
- Height on Wear OS Health Services. None of the 65 Health Services DataType constants (Jetpack reference, read 2026-10-04) has a name or description mentioning “height”: no height type to map.
- Heart rate variability on Wear OS Health Services. None of the 65 Health Services DataType constants (Jetpack reference, read 2026-10-04) has a name or description mentioning “variability”: no heart-rate-variability type to map.
- Blood oxygen saturation (SpO2) on Wear OS Health Services. None of the 65 Health Services DataType constants (Jetpack reference, read 2026-10-04) has a name or description mentioning “spo2” or “saturation”: no oxygen-saturation type to map.
- Records outside v1. Resting heart rate, active energy, distance, nutrition and the rest of each platform’s catalog are not in this version; the converter reports them as unmapped instead of dropping them silently.
How it is generated
The record shapes are this site’s design. Every platform name in a mapping is looked up in a generated dataset, and the generator stops when one is missing, so a renamed field fails the build instead of shipping a stale mapping. The datasets: Apple HealthKit (read 2026-10-02), Android Health Connect (read 2026-10-03), Google Health API (read 2026-10-31), Wear OS Health Services (read 2026-10-04), Workout activity-type crosswalk (read 2026-10-18). Facts the datasets do not hold were read from the vendors’ reference pages: 12 pages, including Health Connect’s HeartRateRecord.Sample and SleepSessionRecord.Stage and Apple’s HKCategoryValueSleepAnalysis and HKUnit.percent() (read 2026-11-05). The workout activity type uses the workout-type crosswalk’s key, and a shared key is a shared name, not a claim that two platforms mean the same activity.
Frequently asked questions
Is there a common schema for HealthKit, Health Connect and Google Health API data?
Not from the platforms: each publishes its own types. This site publishes one, version 1.0.0, as a JSON Schema and TypeScript types at /schema/v1/unified-health-data.schema.json and /schema/v1/unified-health-data.d.ts, generated from its datasets of the platforms' own documentation so every field mapping carries the sentence it rests on.
Why does the unified HRV record carry a metric field instead of one number?
Because the platforms publish different statistics. HealthKit's type is heartRateVariabilitySDNN, the standard deviation of heartbeat intervals; Health Connect's record and the Google Health API's field are RMSSD. They are different measures of the same heartbeat series, and no formula converts one into the other, so the record says which one it is.
How do I convert HealthKit oxygen saturation to the same scale as Health Connect?
Multiply by 100. Apple's HKUnit.percent() documentation says percent measures a value between 0.0 and 1.0, while Health Connect's OxygenSaturationRecord.percentage and the Google Health API's percentage field state a 0 to 100 range. The unified record holds 0 to 100.
Can I use the unified schema in my own product?
Yes, under CC BY 4.0, the same license as this site's datasets: attribute aifitnessapi.com. The vendor sentences quoted as evidence in mappings.json remain their publishers' text. The schema is versioned in its URL, so v1 stays where it is when a later version changes a shape.
From AIFitnessAPI. Related: the HealthKit ↔ Health Connect type reference, normalizing wearable data, Fitbit Web API → Google Health API map and the open datasets.