Skip to content
AF
Open Schema

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 type and a source (platform, nativeType, optional sourceName), 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
time
string
startDate
Date
a sample at one moment has startDate equal to endDate
SourceThe sample’s start date.developer.apple.com
samples[].time
Instant
SourceThe point in time when the measurement was taken.developer.android.com
heartRate.sampleTime.physicalTime
string/google-datetime
SourceRequired. The time of the observation.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.

utcOffset
string | 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.

startZoneOffset
ZoneOffset?
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.utcOffset
string/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.

beatsPerMinute
number · count/min
quantity
HKQuantity (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
SourceHeart beats per minute. Validation range: 1-300.developer.android.com
heartRate.beatsPerMinute
string/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
SampleDataPoint.value
Double
SourceCurrent heart rate, in beats per minute.developer.android.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
startTime
string
startDate
Date
SourceThe sample’s start date.developer.apple.com
startTime
Instant
SourceStart time of the record.developer.android.com
steps.interval.startTime
string/google-datetime
SourceRequired. Observed interval start time.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.

endTime
string
endDate
Date
SourceThe sample’s end date.developer.apple.com
endTime
Instant
SourceEnd time of the record.developer.android.com
steps.interval.endTime
string/google-datetime
SourceRequired. Observed interval end time.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.

startUtcOffset
string | 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.

startZoneOffset
ZoneOffset?
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.startUtcOffset
string/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.

endUtcOffset
string | 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.

endZoneOffset
ZoneOffset?
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.endUtcOffset
string/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.

count
integer · count
quantity
HKQuantity (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
count
Long
SourceCount. Required field. Valid range: 1-1000000.developer.android.com
steps.count
string/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
IntervalDataPoint.value
Long
SourceNumber of steps taken since the last update.developer.android.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
startTime
string
startDate
Date
SourceThe sample’s start date.developer.apple.com
startTime
Instant
SourceStart time of the record.developer.android.com
sleep.interval.startTime
string/google-datetime
SourceRequired. The start time of the observed session.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.

endTime
string
endDate
Date
SourceThe sample’s end date.developer.apple.com
endTime
Instant
SourceEnd time of the record.developer.android.com
sleep.interval.endTime
string/google-datetime
SourceRequired. The end time of the observed session.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.

startUtcOffset
string | 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.

startZoneOffset
ZoneOffset?
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.startUtcOffset
string/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.

endUtcOffset
string | 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.

endZoneOffset
ZoneOffset?
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.endUtcOffset
string/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.

stages
array
value (HKCategoryValueSleepAnalysis)
Int
each sample's startDate, endDate and value become one stage
SourceThe category value for this sample.developer.apple.com
stages[].startTime, stages[].endTime, stages[].stage
List<SleepSessionRecord.Stage>; stage: Int
stage Int → the unified vocabulary (table below)
SourcestartTime: Instant, endTime: Instant, stage: Intdeveloper.android.com
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.

HealthKit
ValueUnifiedPlatform says
HKCategoryValueSleepAnalysisInBedinBedThe user is in bed.
HKCategoryValueSleepAnalysisAwakeawakeThe user is awake.
HKCategoryValueSleepAnalysisAsleepCorelightThe 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.
HKCategoryValueSleepAnalysisAsleepDeepdeepThe user is in deep sleep.
HKCategoryValueSleepAnalysisAsleepREMremThe user is in REM sleep.
HKCategoryValueSleepAnalysisAsleepUnspecifiedasleepThe user is asleep, but the specific stage isn’t known.
HKCategoryValueSleepAnalysisAsleepasleepThe user is sleeping.
Deprecated in iOS 16.0; Apple's availability data names HKCategoryValueSleepAnalysis.asleepUnspecified as its new name.
Health Connect
ValueUnifiedPlatform says
STAGE_TYPE_UNKNOWN (0)unknownUse this type if the stage of sleep is unknown.
STAGE_TYPE_AWAKE (1)awakeThe 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)asleepThe user is asleep but the particular stage of sleep (light, deep or REM) is unknown.
STAGE_TYPE_OUT_OF_BED (3)outOfBedThe user is out of bed and assumed to be awake.
STAGE_TYPE_LIGHT (4)lightThe user is in a light sleep stage.
STAGE_TYPE_DEEP (5)deepThe user is in a deep sleep stage.
STAGE_TYPE_REM (6)remThe user is in a REM sleep stage.
STAGE_TYPE_AWAKE_IN_BED (7)awakeInBedThe user is awake and in bed.
Google Health API
ValueUnifiedPlatform says
SLEEP_STAGE_TYPE_UNSPECIFIEDunknownThe default unset value.
AWAKEawakeSleep stage AWAKE.
LIGHTlightSleep stage LIGHT.
DEEPdeepSleep stage DEEP.
REMremSleep stage REM.
ASLEEPasleepSleep stage ASLEEP.
RESTLESSrestlessSleep 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
startTime
string
startDate
Date
SourceThe sample’s start date.developer.apple.com
startTime
Instant
SourceStart time of the record.developer.android.com
exercise.interval.startTime
string/google-datetime
SourceRequired. The start time of the observed session.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.

endTime
string
endDate
Date
SourceThe sample’s end date.developer.apple.com
endTime
Instant
SourceEnd time of the record.developer.android.com
exercise.interval.endTime
string/google-datetime
SourceRequired. The end time of the observed session.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.

startUtcOffset
string | 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.

startZoneOffset
ZoneOffset?
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.startUtcOffset
string/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.

endUtcOffset
string | 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.

endZoneOffset
ZoneOffset?
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.endUtcOffset
string/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.

activityType
string | null
workoutActivityType
HKWorkoutActivityType
Objective-C constant → crosswalk key
SourceThe type of activity performed during the workout.developer.apple.com
exerciseType
Int
Int value → EXERCISE_TYPE_ constant → crosswalk key
SourceType of exercise (e.g. walking, swimming). Required field.developer.android.com
exercise.exerciseType
string
enum string → crosswalk key
SourceRequired. The type of activity performed during an exercise.developers.google.com
name
String
constant name → crosswalk key
Source85 ExerciseType constants read from the androidx source on 2026-10-18developer.android.com
nativeActivityType
string
workoutActivityType
HKWorkoutActivityType
SourceThe type of activity performed during the workout.developer.apple.com
exerciseType
Int
SourceType of exercise (e.g. walking, swimming). Required field.developer.android.com
exercise.exerciseType
string
SourceRequired. The type of activity performed during an exercise.developers.google.com
name
String
energyKcal
number | 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.caloriesKcal
number/double
SourceOptional. Total calories burned by the user during the exercise.developers.google.com
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.

distanceMeters
number | 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.distanceMillimeters
number/double
÷ 1,000 (the field name states millimeters)
SourceOptional. Total distance covered by the user during the exercise.developers.google.com
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
time
string
startDate
Date
a sample at one moment has startDate equal to endDate
SourceThe sample’s start date.developer.apple.com
time
Instant
SourceTime the record happened.developer.android.com
weight.sampleTime.physicalTime
string/google-datetime
SourceRequired. The time of the observation.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.

utcOffset
string | 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.

zoneOffset
ZoneOffset?
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.utcOffset
string/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.

kilograms
number · kg
quantity
HKQuantity (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
weight
Mass
Mass → kilograms (inKilograms)
SourceUser's weight in kilograms. Required field. Valid range: 0-1000 kilograms.developer.android.com
weight.weightGrams
number/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
time
string
startDate
Date
a sample at one moment has startDate equal to endDate
SourceThe sample’s start date.developer.apple.com
time
Instant
SourceTime the record happened.developer.android.com
bodyFat.sampleTime.physicalTime
string/google-datetime
SourceRequired. The time of the observation.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.

utcOffset
string | 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.

zoneOffset
ZoneOffset?
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.utcOffset
string/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.

percent
number · %
quantity
HKQuantity (percent unit family)
× 100 (Apple: “Percent measures a value between 0.0 and 1.0.”)
SourcePercent measures a value between 0.0 and 1.0.developer.apple.com
percentage
Percentage
SourcePercentage. Required field. Valid range: 0-100.developer.android.com
bodyFat.percentage
number/double
SourceRequired. Body fat percentage. Must be in the range `[0, 100]`.developers.google.com
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
time
string
startDate
Date
a sample at one moment has startDate equal to endDate
SourceThe sample’s start date.developer.apple.com
time
Instant
SourceTime the record happened.developer.android.com
height.sampleTime.physicalTime
string/google-datetime
SourceRequired. The time of the observation.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.

utcOffset
string | 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.

zoneOffset
ZoneOffset?
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.utcOffset
string/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.

meters
number · m
quantity
HKQuantity (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
height
Length
Length → meters (inMeters)
SourceHeight in Length unit. Required field. Valid range: 0-3 meters.developer.android.com
height.heightMillimeters
string/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
time
string
startDate
Date
a sample at one moment has startDate equal to endDate
SourceThe sample’s start date.developer.apple.com
time
Instant
SourceTime the record happened.developer.android.com
heartRateVariability.sampleTime.physicalTime
string/google-datetime
SourceRequired. The time of the observation.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.

utcOffset
string | 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.

zoneOffset
ZoneOffset?
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.utcOffset
string/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.

metric
string
(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.rootMeanSquareOfSuccessiveDifferencesMilliseconds
rmssd
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.

milliseconds
number · ms
quantity
HKQuantity (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
heartRateVariabilityMillis
Double
SourceHeart rate variability in milliseconds. Required field. Valid Range: 1-200.developer.android.com
heartRateVariability.rootMeanSquareOfSuccessiveDifferencesMilliseconds
number/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
time
string
startDate
Date
a sample at one moment has startDate equal to endDate
SourceThe sample’s start date.developer.apple.com
time
Instant
SourceTime the record happened.developer.android.com
oxygenSaturation.sampleTime.physicalTime
string/google-datetime
SourceRequired. The time of the observation.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.

utcOffset
string | 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.

zoneOffset
ZoneOffset?
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.utcOffset
string/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.

percent
number · %
quantity
HKQuantity (percent unit family)
× 100 (Apple: “Percent measures a value between 0.0 and 1.0.”)
SourcePercent measures a value between 0.0 and 1.0.developer.apple.com
percentage
Percentage
SourcePercentage. Required field. Valid range: 0-100.developer.android.com
oxygenSaturation.percentage
number/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 · platformFromToFactor and source
Workout session · Google Health APIexercise.metricsSummary.distanceMillimetersdistanceMeters× 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 APIweight.weightGramskilograms× 0.001 — ÷ 1,000
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). source
Body fat percentage · HealthKitquantitypercent× 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 APIheight.heightMillimetersmeters× 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) · HealthKitquantitypercent× 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.