Parse, validate, import/export GEDCOM files (5.5.1 and 7.0). Use for .ged files, genealogy data interchange, or GEDCOM compliance.
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Parse, validate, import/export GEDCOM files (5.5.1 and 7.0). Use for .ged files, genealogy data interchange, or GEDCOM compliance.
GEDCOM
â ïž Prime Directive: Import What's in the File, Nothing More
GEDCOM imports are a write path â they bring data into the database from the source file. Per the data-fidelity prime directive in CLAUDE.md, the importer must:
Preserve verbatim what the source file contains.MAP > LATI/LONG sub-tags get persisted to places.latitude/longitude because the file already had those coordinates â they came with the file the user imported. That's authored data being preserved.
NEVER fill in fields the source did not contain. Don't infer date_type='exact' because a DATE line had a parseable string. Don't auto-resolve PLAC "Stockholm" to coordinates via a gazetteer and persist them â let the resolver compute coords at view time.
NEVER fix obvious errors silently. A typo in the source GEDCOM stays as a typo. The user can clean their data after import; the importer's job is fidelity, not correction.
Export has the mirror obligation: round-trip what the database stores, with explicit excluded[] reporting for entities GEDCOM 5.5.1 cannot represent. Don't synthesize plausible-looking values to "complete" the export.
This rule is non-negotiable. Past violations corrupted real databases.
GEDCOM (Genealogical Data Communication) is the de facto standard for exchanging family tree data between genealogy applications.
Versions
GEDCOM 5.5.1 (1999, updated 2019) â dominant format; supported by virtually all genealogy software including Ancestry, FamilySearch, MyHeritage, Gramps
GEDCOM-X â JSON/XML format from FamilySearch; their API uses it; rarely used as a file format
Target 5.5.1 for maximum compatibility.
File structure
0 HEAD
1 GEDC
2 VERS 5.5.1
1 CHAR UTF-8
0 @I1@ INDI
1 NAME Lars /Eriksson/
1 SEX M
1 BIRT
2 DATE 15 MAR 1842
2 PLAC Björkvik, Rönö hÀrad, Södermanland
0 @F1@ FAM
1 HUSB @I1@
1 WIFE @I2@
1 CHIL @I3@
1 MARR
2 DATE 14 JUN 1869
0 TRLR
83 person fact types (Religion, Nationality, Physical description, National ID, Multiple birth, etc.)
Multi-language place names (GEDCOM-X PlaceDescription.names list)
Place identifiers (GeoNames ID, Wikidata Q-numbers)
Lower priority:
Name qualifiers beyond patronymic/particle (Middle, Familiar, Religious, Geographic, Occupational, etc.)
Multiple name forms with script/language variants (for non-Latin names)
Source rights, coverage, media type, publisher/mediator agents
Date precision beyond current model (time of day, timezones, recurring dates, durations)
Attribution on individual data elements (who added which name variant)
Dialect coverage
Major genealogy apps emit GEDCOM with their own dialect quirks. The repo carries two layers of test coverage to keep the GEDCOM importer honest about them:
Synthetic dialect fixtures at tests/fixtures/gedcom/dialects/ â one minimal .ged per app (RootsMagic, Gramps, Family Tree Maker, Legacy, MacFamilyTree, Family Historian, MyHeritage, PAF, Holger, Genney) carrying that app's 1 SOUR signature plus characteristic custom tags. tests/unit/import-gedcom-dialects.test.ts asserts each imports without crashing and that no core GEDCOM tag (NAME, INDI, FAM, BIRT, DEAT, MARR, SEX, âŠ) ends up in the report's skipped list.
Real-world samples (gitignored) at export-import/samples/:
familysearch-gedcom7/ â official FamilySearch GEDCOM 7.0 reference test files (24 files; download via https://raw.githubusercontent.com/FamilySearch/GEDCOM.io/main/testfiles/gedcom70/<name>.ged)
gramps/sample.ged â Gramps's official sample, real Gramps export
heiner-torture/allged.ged â Heiner Eichmann's GEDCOM 5.5 torture test
d-jeffrey/ â 12 real exports tagged by source app (RootsMagic 8 Queen 4683 persons; FTM 20 Habsburg 34020 persons; PAF 2.2 royal92; Legacy ivar/tudor; FamilyOrigins washington; ANCESTRIS bourbon/kennedy)
The diagnostic loop when adding new tag handling: walk all 36 samples through the importer, eyeball the per-file skipped-tag list, decide for each tag whether it's a real loss to fix (e.g. _UID/AFN/SSN/FSID â person identifiers, CREM/BARM/ANUL/MARL/_SEPR â new event types) or an intentional drop (LDS ordinances, app-internal flags like _UPD / _PHOTO / _PPEXCLUDE).
After every importer change, the canonical "did anything break" check is: rerun the dialect tests + walk the real samples once more. If a previously-handled tag falls into the skipped list on a real sample, that's a regression.
Date format
GEDCOM 5.5.1 â app model
GEDCOM date string
date_type
date_value
date_value_end
12 JUN 1845
exact
1845-06-12
null
JUN 1845
exact
1845-06
null
1845
exact
1845
null
ABT 1845 / CAL 1845 / EST 1845
about
1845
null
BEF 1850
before
1850
null
AFT 1840
after
1840
null
BET 1840 AND 1850
between
1840
1850
FROM 1840 TO 1850
between
1840
1850
Any other string
unknown
null
null
Always preserve the original GEDCOM date string in date_original.
App model â GEDCOM 5.5.1
Use date_original if set (preserves the original format). Otherwise reconstruct from date_type + date_value.
PLAC (place) handling
GEDCOM 5.5.1 PLAC is a free-text comma-separated hierarchy: "Björkvik, Rönö hÀrad, Södermanland, Sverige".
On import: Call findOrCreatePlace(name) for the full string. The hierarchy is preserved in the name field as-is. Parsing and linking hierarchy levels is optional post-processing.
On export: Use place.name directly as the PLAC value.
ADDR (address) handling
GEDCOM 5.5.1 has no ADDR directly on PLAC tags. The ADDR structure appears on the containing event (e.g. RESI):
1 RESI
2 PLAC TvÀrgatan 5, VÀxjö, Sverige
2 ADDR TvÀrgatan 5
3 ADR1 TvÀrgatan 5
3 CITY VÀxjö
3 POST 35243
3 CTRY Sverige
On import: When a RESI (or other event) has both PLAC and ADDR, populate the address columns on the corresponding place record from ADR1 â street, CITY â city, POST â postal_code, CTRY â country. Use updatePlace(db, place.id, { street, city, postal_code, country }).
On export: If a place has street set, emit ADDR/ADR1/CITY/POST/CTRY below the event's PLAC tag.
NAME format
GEDCOM NAME format: Given /Surname/ Suffix or just Given /Surname/
On import:
const match = raw.match(/^(.*?)\/(.+?)\/(.*)?$/);
const given = match?.[1]?.trim() || null;
const surname = match?.[2]?.trim() || null;
On export:
1 NAME Lars /Eriksson/
2 NPFX von â if name_prefix set
2 NSFX Jr. â if name_suffix set
Essential tags by record type
Individual (INDI)
Tag
Meaning
NAME
Full name; surname in slashes: Lars /Eriksson/
SEX
M, F, U
BIRT
Birth event
DEAT
Death event
BURI
Burial
CHR / BAPM
Christening / Baptism
CONF
Confirmation
OCCU
Occupation
RESI
Residence
EMIG / IMMI
Emigration / Immigration
NATU
Naturalization
CENS
Census
EDUC
Education
GRAD
Graduation
RETI
Retirement
PROB / WILL
Probate / Will
NOTE
Free-text note
SOUR
Source citation
REFN
Reference number (external ID)
RIN
Record ID number
FAMC
Family as child (pointer to FAM)
FAMS
Family as spouse (pointer to FAM)
Family (FAM)
Tag
Meaning
HUSB
Pointer to husband INDI
WIFE
Pointer to wife INDI
CHIL
Pointer to child INDI (repeat for each child)
MARR
Marriage event
DIV
Divorce event
CENS
Census (family)
Events (sub-records of INDI or FAM)
Tag
Meaning
DATE
Event date
PLAC
Place (comma-separated hierarchy)
NOTE
Note about this event
SOUR
Source citation for this event
Source citation (SOUR sub-record)
1 SOUR @S1@
2 PAGE p. 42
2 DATA
3 TEXT verbatim text from source
2 QUAY 2
QUAY = 0 (unreliable) to 3 (direct primary evidence) â maps directly to citations.confidence.
Source record (SOUR at level 0)
0 @S1@ SOUR
1 TITL 1880 Swedish Church Records
1 AUTH Riksarkivet
1 PUBL Stockholm
1 REPO @R1@
Swedish church archive reference patterns
Swedish GEDCOM files (especially from Genney/ArkivDigital) use a structured source title format:
REPO records map to link targets: ArkivDigital â app.arkivdigital.se, Riksarkivet â sok.riksarkivet.se, Sveriges slĂ€ktforskarförbund â genealogi.se (Dödboken, Sveriges Befolkning).
The source linker (src/api/source-linker.ts) auto-detects these patterns and renders inline links. See src/api/link-rules/sv.ts for the regex rules.
GEDCOM 7.0 key differences
UTF-8 mandatory (no ANSEL/ASCII)
Structured dates with calendar systems (Gregorian, Julian, Hebrew, French Republican)
SNOTE (shared note reference)
INDI.NO (negative assertion â "this person had no children")
Extension mechanism via SCHMA tag (custom tags without _ prefix)
Removes CONC line concatenation
Typed relationships (GEDCOM 7.0 uses enumerated PEDI values for parent-child)
Parsing GEDCOM
A minimal line-by-line parser:
Split each line: LEVEL [XREF] TAG [VALUE]
Maintain a stack to track the current record hierarchy
When level drops, pop the stack
Accumulate CONT lines into the previous value (with \n)
CONC (removed in 7.0) concatenates without newline â handle for compatibility
TypeScript libraries for this project:
read-gedcom â browser and Node.js compatible, parses to a queryable tree
gedcom-stream â streaming parser, good for large files (100MB+)
Data integrity rule
Data must never be silently lost during import or export. This is fundamental to user trust.
Every piece of data that is dropped, skipped, remapped, or excluded must be reported to the user with:
What was dropped (tag name or entity type)
How many records/occurrences
Why (no app concept, redundant with standard tag, model limitation, etc.)
Use the ImportReport fields for import, and a pre-export warning for export:
unmappedData: { category, count }[] â known-unsupported record types with human-readable explanation
warnings: string[] â data that was transformed/remapped rather than dropped straight (e.g. "6573 REMA remarks imported as person notes")
On export: if any DB entities cannot be represented in GEDCOM (e.g. Research Tasks, Groups), the export function must return a summary of what was excluded and why, so the UI can show it to the user.
Never use unmappedData/warnings for import and then omit the equivalent for export. Both directions must be transparent.
What counts as "reported"
Situation
Required
Tag appears in file but has no DB mapping
skipped entry with tag + count
Known record type with no app concept (REPO, SUBM)
unmappedData entry with description
Data remapped/converted (e.g. TRAN â aka name)
warnings entry explaining conversion
DB entity type that cannot be exported to GEDCOM
Export summary entry with count + reason
Data actively handled but user may not expect it
warnings entry (e.g. REMA imported as notes)
What is NOT required
Field-level losses where the standard itself has no concept (e.g. no GEDCOM 5.5.1 tag for persons.living) â document in SKILL.md "What is dropped" table instead
Entirely empty tables (no records to export)
Export checklist
0 HEAD with 1 GEDC + 2 VERS 5.5.1 + 1 CHAR UTF-8
Valid XREF IDs (@I1@, @F1@, @S1@, etc.)
0 TRLR at end
Standard date format (preserve date_original if set)
HUSB/WIFE in FAM â FAMS back-references on INDI (optional but standard)
CHIL in FAM â FAMC back-references on INDI (optional but standard)
Return export summary listing any DB tables/fields that were excluded from the output
Import checklist
Parse all level-0 record types: INDI, FAM, SOUR (skip REPO, SUBM, OBJE)
Resolve XREF pointers (@I1@, @F1@, etc.) using a map
Handle NAME tag: parse Given /Surname/ + NPFX + NSFX
Parse all date formats (exact, ABT, BEF, AFT, BET AND, FROM TO)
Call findOrCreatePlace(name) for all PLAC values
Preserve original date string in date_original
Concatenate CONT lines with \n
Process in order: SOUR â INDI â FAM (to resolve forward references)
Each FAM.CHIL creates two parent_child relationships (one per parent, if both present)
All skipped/remapped data reported per data integrity rule above
Common pitfalls
Name format: Some apps export Given /Surname/ Suffix; others omit the slashes. Handle gracefully.
Unknown parents: A FAM may have only WIFE or only HUSB, or neither. Still create the couple relationship.
Non-standard tags: Apps use _MILI, _FSID, etc. Don't crash â report in skipped, never drop silently.
Encoding: Older files may use ANSEL. Detect from HEAD.CHAR tag.
Large files: Some GEDCOM files are 100MB+. Stream-parse; don't load into memory.
CONT at level 2+: CONT applies to the immediately preceding sibling at the same depth, not just level-1 values.
Empty PLAC values: Many apps export 2 PLAC with no value. Skip these; don't call findOrCreatePlace('').
Round-trip fidelity registry
Every column in every non-exempt table has an entry in src/api/gedcom_fidelity_registry.ts declaring its round-trip status under GEDCOM 5.5.1 and 7.0. Three tests enforce the contract:
tests/unit/gedcom-fidelity-registry-coverage.test.ts â schema-introspection guard. Adding a column without a registry entry fails CI.
When you change anything in src/import/gedcom/ or src/gedcom/exporter.ts, run those three tests. When you add a schema column, the coverage test will tell you to register it.
See CLAUDE.md "â ïž Prime Directive (cont.): Round-Trip Fidelity" for the why.