Organization relationships
An organization record often needs to point to other organizations: the network a local chapter belongs to, the foundation that fiscally sponsors a project, the organization that carried on its work after a merger, or the record that a duplicate record should give way to. ADR 0023 decided what a reference to another organization carries: its id, name, and optional identifiers. This decision covers where those references go, how a relationship’s type is stated, and what a relationship can say about itself, such as when it started.
Here is a literacy project that is a chapter of a national network and is fiscally sponsored by a local foundation:
{ "id": "01912a8b-7c3d-7890-abcd-ef1234567890", "name": "Riverside Reading Project", "relationships": { "parents": [ { "org": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "National Reading Network" }, "kind": { "value": "chapter" } }, { "org": { "id": "01912a8b-7c3d-7892-abcd-ef1234567892", "name": "Riverside Community Foundation" }, "kind": { "value": "fiscalSponsor" }, "startDate": "2024-07-01", "status": "active" } ] }}The list name, parents, gives the relationship’s category and direction: each org is above this organization. kind optionally narrows that to a subtype. org identifies the other organization, while startDate, endDate, and status describe the relationship itself.
Decision
Section titled “Decision”We add an optional relationships object to OrganizationBase with seven optional lists. Every entry in every list is a relationship object with a required org and optional kind, startDate, endDate, and status. Other names the same organization goes by, such as a trading name or a short form of its legal name, stay on the organization in an optional aliases list of non-empty, unique strings, using a new public v0.5 scalar, Types.nonEmptyString. They are not relationships or a history of former names. A name that has its own live trading-name record linked to this organization by a dba entry in either record, parents on the trading-name record or children on this one, shouldn’t also be listed in aliases, so readers don’t count it twice. A name can go in aliases when no live dba record for it remains, for example when that record was replaced, as a duplicate or merged record, by the legal entity’s own record. The schema doesn’t check this.
| List | Category | Each entry says | Standard kind values |
|---|---|---|---|
parents |
Hierarchy | org is above this organization |
chapter, department, branch, subsidiary, fiscalSponsor, dba |
children |
Hierarchy | org is below this organization |
Same as parents |
succeededBy |
Succession | org carries on all or part of this organization |
merger, acquisition, divestiture, split |
succeeds |
Succession | This organization carries on all or part of org |
Same as succeededBy |
recordReplacedBy |
Record | org’s record should be used instead of this record |
duplicate, merged |
recordReplaces |
Record | This record should be used instead of org’s record |
Same as recordReplacedBy |
otherRelationships |
Other | This organization is connected to org, possibly with no direction |
None |
Every list also accepts a custom kind. List order carries no meaning, such as a primary parent.
- Positive consequences
- A consumer following parents or children reads one list per organization and finds every entry the provider supplied there, whatever subtype each entry uses.
- Each named list gives its entries’ category and direction, so an entry there with no
kind, or with akindthe consumer doesn’t recognize, is still usable.otherRelationshipsentries need neither a direction nor akind. - Dates and status describe the relationship without being mixed into the other organization’s identity.
- A subtype can repeat, such as the two organizations formed by a split.
- A provider can state a relationship from whichever organization’s record it maintains.
- Negative consequences
- Under JSON Merge Patch, changing one entry means sending the whole list again. A writer that understands only some entries must still send the rest unchanged, and two writers updating the same list can overwrite each other.
- Reading only one subtype means filtering the list.
- Each entry nests the reference one level deeper, under
org. - Because both directions can be stated, two records can disagree. The protocol doesn’t require them to match.
- Consumers can’t assume one parent per subtype or a
kindon every entry. - A target must already exist in the receiver. Identifiers sent with a target don’t resolve it; only its
iddoes.
Criteria
Section titled “Criteria”Required capabilities and boundaries screen out shapes before preferences are weighed:
- Keep the reference content from ADR 0023.
- Keep hierarchy, succession, and record replacement distinct, with room for other connections.
- Allow several relationships in a category at once, whether they have different subtypes (a chapter parent and a fiscal sponsor) or the same one (the two organizations formed by a split).
- Use the existing Merge Patch semantics, with no new patch mechanism.
- Limit the change to organizations. Opportunities, awards, and
Award.parentare unchanged, and organization status is handled separately in PR 1252.
Preferences, from most to least weight:
- A consumer can follow the supplied parents or children across mixed subtypes without knowing each level’s label. This is the deciding preference.
- Facts about a relationship stay separate from facts about the other organization.
- A writer can change one category without resending the others.
- A writer can change one subtype without resending the others.
- A consumer can read one subtype without filtering.
- The public model stays small.
Options considered
Section titled “Options considered”| Option | Result | Deciding reason |
|---|---|---|
| Named single references | Rejected | One target per field can’t hold a split or several parents, and there’s no place for dates or status. |
| Parents grouped by kind | Rejected | Allows one-kind writes, but a consumer following parents must gather every group. |
Lists with kind beside the reference’s fields |
Rejected | Walks well, but mixes relationship facts into the other organization’s fields. |
| One list for every relationship | Rejected | Every change sends every relationship again, and each entry must carry its category, plus its direction where it has one. |
Lists by category and direction, with org wrapped |
Selected | Mixed subtypes read from one list, separate relationship facts, and independent categories. |
Representation
Section titled “Representation”/** Subtypes of a parent or child relationship */enum OrgHierarchyKindOptions { chapter, department, branch, subsidiary, fiscalSponsor, dba, custom,}
/** Subtypes of organizational succession */enum OrgSuccessionKindOptions { merger, acquisition, divestiture, split, custom,}
/** Subtypes of record replacement */enum OrgRecordKindOptions { duplicate, merged, custom,}
/** Other relationships define no standard subtypes */enum OrgOtherKindOptions { custom,}
/** Whether a relationship is in effect, as stated by the provider */enum OrgRelationshipStatus { active, inactive,}
/** A relationship from this organization to another organization */model OrgRelationshipT<TKind> { /** The other organization */ org: OrgRef;
/** The relationship's subtype within the category its list names */ kind?: Fields.ExtensibleEnumT<TKind>;
/** When the relationship started */ startDate?: Types.isoDate;
/** When the relationship ended */ endDate?: Types.isoDate;
/** Whether the relationship is in effect, as stated by the provider */ status?: OrgRelationshipStatus;}
/** An organization's relationships to other organizations */model OrgRelationships { /** Organizations above this one, such as a containing organization or a fiscal sponsor */ parents?: OrgRelationshipT<OrgHierarchyKindOptions>[];
/** Organizations below this one, such as its chapters or sponsored projects */ children?: OrgRelationshipT<OrgHierarchyKindOptions>[];
/** Organizations that carry on all or part of this one */ succeededBy?: OrgRelationshipT<OrgSuccessionKindOptions>[];
/** Organizations whose work this one carries on in whole or in part */ succeeds?: OrgRelationshipT<OrgSuccessionKindOptions>[];
/** Records to use instead of this record */ recordReplacedBy?: OrgRelationshipT<OrgRecordKindOptions>[];
/** Records that this record is used instead of */ recordReplaces?: OrgRelationshipT<OrgRecordKindOptions>[];
/** Connections outside the categories above, which need not have a direction */ otherRelationships?: OrgRelationshipT<OrgOtherKindOptions>[];}
model OrganizationBase { // ...existing fields
/** Other names this organization goes by, such as a trading name or a short form of its legal name. Not a history of former names. A name with its own live `dba` record shouldn't also be listed here */ @uniqueItems aliases?: nonEmptyString[];
/** The organization's relationships to other organizations */ relationships?: OrgRelationships;}OrgRef is the existing reference model. org and list entries are required. Every other member is optional and, like most optional fields on existing models, isn’t nullable: a reader finds it omitted or with a value, never null. Model names, and the use of one template for every list, are implementation choices; this ADR decides the wire shape.
OrgRef.name is reworded to “The name this organization is known by; for a legal entity, its legal name,” because a trading-name record’s name isn’t a legal name. It stays a required string. The rewording also reaches OrganizationBase.name and appears in the published v0.4.0 docs; it changes only the description.
Relationship objects
Section titled “Relationship objects”orgis required. On reads it is anOrgRef. BecauseOrgRef.idis read-only, the write shape keepsorg.idwritable, and a writer can identify a target byidalone. The receiver rejects anidit doesn’t recognize. A target’snameandidentifiersare optional on writes and ignored if supplied: the receiver fills them in from its own record on reads and revision snapshots. They stay accepted, rather than read-only, so a writer can resend entries copied from a read. AnOrgRevision’spatchis recorded as received, so a targetnameoridentifiersthere isn’t authoritative; reads and the revision’ssnapshotcarry the filled-in values.kindis optional in every list, includingotherRelationships. It uses the protocol’s extensible enum: either a standardvalue, or"value": "custom"with the subtype incustomValue. As elsewhere in the protocol,descriptionis optional. A custom kind in a named list adds a subtype to that list’s category.otherRelationshipsdefines no standard values, so a kind stated there is always custom.startDateandendDateare optional ISO dates, with the same names and type asDateRangeEvent.statusis optional and isactiveorinactive. It describes the relationship, not either organization. It has no default and isn’t derived from the dates: an entry with a pastendDateand nostatushasn’t stated a status. The protocol requires no particular combination of dates and status.
An entry with only org is valid in every list.
Why one list per category and direction
Section titled “Why one list per category and direction”The deciding consideration is how consumers follow relationships across levels. Subtypes change from level to level, and providers won’t always label the same level the same way. A federal department might contain an agency, which contains a center, which contains a division, while a sibling agency uses institutes, offices, and branches at the same depths. A consumer walking up from a division can’t know which label the next level uses, so it reads every parent the record supplies anyway. If parents were grouped by kind, that consumer would have to gather every group, including custom groups it has never seen, and a provider’s choice of label would change where a parent appears in the payload. In a single list, the label is a value on the entry, and a provider that is unsure of the subtype can leave it out.
The cost falls on writes. ADR 0026 uses JSON Merge Patch, which replaces arrays whole. Grouping by kind would let a writer replace one kind’s parents without touching the others; a single list makes the writer send every parent. Updating one kind on its own is a convenience rather than a requirement, since a writer can still make any change by sending the full list.
Separate lists keep categories independent: changing parents never touches succession or record entries, and a consumer walking a hierarchy never filters them out.
org wraps the reference instead of placing kind, dates, and status beside its fields. The reference describes the other organization; the remaining fields describe the relationship.
Hierarchy
Section titled “Hierarchy”parentsandchildrencover more than containment.fiscalSponsoris a hierarchy kind, so one organization can list a chapter parent and a different fiscal sponsor in the sameparentslist. Consumers shouldn’t treat every parent as an owner or container.- There is no generic
parentkind. An entry withoutkindalready states a parent relationship without a subtype. - Providers can use a custom kind for other organizational labels, such as division or agency, or omit
kindwhen no subtype is known. dbais for systems that keep a separate record for each trading name. Inparents, it means this record is a name the referenced legal entity does business under; inchildren, that the referenced record is a trading-name record of this one. Most systems don’t keep such records and list the names in the legal entity’saliasesinstead. A name that has its own live trading-name record linked to this organization by adbaentry in either record,parentson the trading-name record orchildrenon this one, shouldn’t also be listed inaliases, so readers don’t count it twice. A name can go inaliaseswhen no livedbarecord for it remains, for example when that record was replaced, as aduplicateormergedrecord, by the legal entity’s own record. The schema doesn’t check this.
Succession and record replacement
Section titled “Succession and record replacement”Succession is about organizations; record replacement is about records in a provider’s system. A merger in succeededBy means organizations combined, while a merged entry in recordReplacedBy means a provider combined records. A provider’s workflow may treat duplicate and merged records alike; the protocol doesn’t require the two to be exclusive. This decision doesn’t prescribe how a provider merges, deletes, or redirects records.
Both directions
Section titled “Both directions”Each named category has a list for each direction, and a provider can expose either or both. The protocol doesn’t require reverse entries, complete lists, or agreement between the two sides, and it doesn’t ask providers to store or maintain a relationship graph. otherRelationships is a single list because its connections need not have a direction.
Updates
Section titled “Updates”ADR 0026’s JSON Merge Patch applies unchanged:
| Patch body | Effect |
|---|---|
{ "relationships": { "parents": [...] } } |
Replaces the whole parents list; other lists are unchanged |
{ "relationships": { "parents": [] } } |
Leaves an empty parents list; other lists are unchanged |
{ "relationships": { "parents": null } } |
Removes parents; other lists are unchanged |
{ "relationships": null } |
Removes every relationship list |
To change one relationship, a writer sends the full list with that entry updated. This patch ends the sponsorship from the opening example and leaves every other list unchanged:
{ "relationships": { "parents": [ { "org": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891" }, "kind": { "value": "chapter" } }, { "org": { "id": "01912a8b-7c3d-7892-abcd-ef1234567892" }, "kind": { "value": "fiscalSponsor" }, "startDate": "2024-07-01", "endDate": "2026-06-30", "status": "inactive" } ] }}This patch names its targets by id alone, which the write shape allows. Reads still return a full OrgRef for each org: the receiver fills in each target’s name and identifiers from its own record, ignores any sent with the id, and rejects an id it doesn’t recognize. Schema validation only checks shape; it doesn’t look up or fill in a target.
In a patch, null removes the addressed member, as in JSON Merge Patch; reads never carry null. Because a patch replaces a list whole, an entry in that list leaves out an optional member rather than setting it to null.
Evaluation
Section titled “Evaluation”Side-by-side
Section titled “Side-by-side”- ✅ Criterion met
- ❌ Criterion not met
- 🟡 Partially met or requires additional handling
| Criterion | Weight | Named single | Grouped by kind | Kind beside fields | One list | Selected |
|---|---|---|---|---|---|---|
| Distinct categories | Required | ✅ | ✅ | ✅ | 🟡 | ✅ |
| Several relationships per category | Required | ❌ | ✅ | ✅ | ✅ | ✅ |
| Existing Merge Patch semantics | Required | ✅ | ✅ | ✅ | ✅ | ✅ |
| Follow supplied mixed subtypes | Deciding | 🟡 | 🟡 | ✅ | 🟡 | ✅ |
| Relationship facts kept separate | Preference | ❌ | ❌ | 🟡 | ✅ | ✅ |
| Change one category alone | Preference | ✅ | ✅ | ✅ | ❌ | ✅ |
| Change one subtype alone | Preference | ✅ | ✅ | ❌ | ❌ | ❌ |
| Read one subtype without filtering | Preference | ✅ | ✅ | ❌ | ❌ | ❌ |
| Small public model | Preference | ✅ | 🟡 | 🟡 | ✅ | 🟡 |
There is no checkmark total or equally weighted score. Named single references fail a required capability. Among the rest, the selected shape and the kind-beside-fields shape are the only ones that read mixed subtypes from one list, and the selected shape also keeps relationship facts apart from organization facts.
Option 1: Named single references — Rejected
Section titled “Option 1: Named single references — Rejected”ADR 0023’s single parent, extended with one field per other relationship:
{ "parent": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "National Reading Network" }, "fiscalSponsor": { "id": "01912a8b-7c3d-7892-abcd-ef1234567892", "name": "Riverside Community Foundation" }}- Pros
- Smallest model; each relationship is read or written by name.
- Fits organizations with at most one relationship of each type.
- Cons
- Can’t hold two parents, or the two organizations formed by a split, without changing a field’s type later.
- No place for a relationship’s subtype, dates, or status.
- A consumer walking upward checks each named field.
Option 2: Parents grouped by kind — Rejected
Section titled “Option 2: Parents grouped by kind — Rejected”An earlier draft of this ADR selected this shape: parents is an object that maps each kind to a list of references, with custom kinds in an otherParents map, and sponsorship, succession, and duplicates as single references:
{ "relationships": { "parents": { "chapter": [ { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "National Reading Network" } ], "otherParents": { "region": [ { "id": "01912a8b-7c3d-7895-abcd-ef1234567895", "name": "Western Region" } ] } }, "fiscalSponsor": { "id": "01912a8b-7c3d-7892-abcd-ef1234567892", "name": "Riverside Community Foundation" } }}- Pros
- Replacing one kind’s list leaves the other kinds unchanged.
- Reading a known kind returns that kind’s entries directly.
- Cons
- A consumer following parents must gather every standard and custom group, including groups it doesn’t recognize.
- A provider’s choice of label decides where a parent appears, so the same level can land in different places from different providers.
- Every parent needs a key, so a parent with no known subtype needs a generic group of its own.
- As drafted, values are bare references with no dates or status. Grouping relationship objects instead would fix that, but not the walk.
Option 3: Lists with kind beside the reference’s fields — Rejected
Section titled “Option 3: Lists with kind beside the reference’s fields — Rejected”The same lists as the selected option, but each entry is the reference itself with relationship fields added:
{ "relationships": { "parents": [ { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "National Reading Network", "kind": { "value": "chapter" } }, { "id": "01912a8b-7c3d-7892-abcd-ef1234567892", "name": "Riverside Community Foundation", "kind": { "value": "fiscalSponsor" }, "status": "active" } ] }}- Pros
- One less level of nesting than the selected option.
- Reads mixed subtypes from one list, as the selected option does.
- Cons
status, dates, andkindsit beside the organization’sid,name, andidentifiers, sostatusreads as if it were the organization’s own status.- A field later added to
OrgRefcould collide with a relationship field.
Option 4: One list for every relationship — Rejected
Section titled “Option 4: One list for every relationship — Rejected”{ "relationships": [ { "category": "parent", "org": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "National Reading Network" }, "kind": { "value": "chapter" } }, { "category": "succeededBy", "org": { "id": "01912a8b-7c3d-7893-abcd-ef1234567893", "name": "Riverside Reading East" }, "kind": { "value": "split" } } ]}- Pros
- Every supplied relationship is in one place.
- Smallest container.
- Cons
- Changing any relationship replaces all of them under Merge Patch, so ending a sponsorship also sends every succession and record entry again.
- Each entry must carry its category, plus its direction where it has one, and consumers filter by category before following parents or children.
Option 5: Lists by category and direction, with org wrapped — Selected
Section titled “Option 5: Lists by category and direction, with org wrapped — Selected”The opening example shows mixed parent subtypes. A split lists both resulting organizations in succeededBy:
{ "relationships": { "succeededBy": [ { "org": { "id": "01912a8b-7c3d-7893-abcd-ef1234567893", "name": "Riverside Reading East" }, "kind": { "value": "split" }, "startDate": "2026-01-01" }, { "org": { "id": "01912a8b-7c3d-7894-abcd-ef1234567894", "name": "Riverside Reading West" }, "kind": { "value": "split" }, "startDate": "2026-01-01" } ] }}- Pros
- Each named list holds the supplied relationships of one category and direction, whatever their subtypes;
otherRelationshipsholds connections outside those categories. - Categories update independently.
- Relationship dates and status have a place that can’t be confused with the other organization’s fields.
- Each named list holds the supplied relationships of one category and direction, whatever their subtypes;
- Cons
- Changing one entry means sending its whole list.
- Reading one subtype requires filtering.
- Entries nest the reference under
org.
Direction options
Section titled “Direction options”| Representation | Result | Tradeoff |
|---|---|---|
| Both directions for each named category | Selected | A relationship can be stated on either organization’s record; the two sides needn’t match. |
One direction only (parents, succeededBy, recordReplacedBy) |
Rejected | Smaller, but a provider that records relationships from the other side has nowhere to put them. |
A network can list its chapters without each chapter listing the network:
{ "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "National Reading Network", "relationships": { "children": [ { "org": { "id": "01912a8b-7c3d-7890-abcd-ef1234567890", "name": "Riverside Reading Project" }, "kind": { "value": "chapter" } } ] }}Fiscal sponsorship options
Section titled “Fiscal sponsorship options”| Representation | Result | Tradeoff |
|---|---|---|
fiscalSponsor kind in parents and children |
Selected | Sponsors appear in the same walk as other parents; consumers can’t assume every parent contains its child. |
Separate sponsorship lists, such as fiscallySponsoredBy and fiscalSponsorOf |
Rejected | Keeps sponsorship apart from structure, but adds a list pair for one subtype that an upward walk must also read. |
The rejected shape would look like this:
{ "relationships": { "parents": [ { "org": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "National Reading Network" }, "kind": { "value": "chapter" } } ], "fiscallySponsoredBy": [ { "org": { "id": "01912a8b-7c3d-7892-abcd-ef1234567892", "name": "Riverside Community Foundation" } } ] }}Operational consequences
Section titled “Operational consequences”Optional additions don’t guarantee compatibility with older validators that reject unknown properties or enum values. Versioning and rollback must not silently discard relationship data.
Existing OrgSync authorization governs changes to an organization’s record; a relationship grants no authority over its target. ADR 0023’s boundary of publicly available identifiers still applies, so a provider avoids exposing target details the reader isn’t authorized to see. Existing change-ledger behavior applies.
Protocol conformance
Section titled “Protocol conformance”| Aspect | Convention | Conforms / Diverges |
|---|---|---|
| Pagination | ADR 0011 list pagination | Unchanged; no new route or pagination fields |
| Identifiers | ADR 0023 OrgRef: id, name, optional identifiers |
Conforms on reads; writes keep org.id writable |
| Headers | ADR 0026 authorization and Merge Patch content type | Conforms; unchanged |
| Field names | camelCase | Conforms |
| Dates | startDate and endDate as isoDate, as in DateRangeEvent |
Conforms; both optional here |
| Subtypes | Extensible enum value, customValue, description |
Conforms |
| Status | Closed enum for a sub-object’s lifecycle, as IdentifierStatus |
Conforms; values active and inactive |
| Response shapes | Existing OrgSync envelopes and revisions | Unchanged |
| Read nullability | Optional fields not nullable, as on most existing models | Conforms; no member accepts null on reads |
| Updates | ADR 0026 JSON Merge Patch | Conforms; lists replace whole |
| Parent location | ADR 0023 parent on the organization |
Diverges; see below |
other prefix |
other<Plural> maps keyed by label, such as otherIds |
Diverges for otherRelationships; see below |
Parent location
Section titled “Parent location”ADR 0023 decided a single parent reference on the organization. This decision moves parents into relationships.parents, where each entry wraps the same reference under org. A single parent holds one target with no subtype or dates: it can’t hold a chapter parent and a fiscal sponsor together, or say when a sponsorship ended. The change serves vendor engineers whose organizations have more than one parent, and the consumers who walk those structures.
ADR 0023 now opens with a notice that points here, and its example shows the new location. Its reference content and identifier decisions stay in force. Award.parent is a separate award concept and is unchanged.
Lesson: what a reference carries doesn’t settle how many relationships an organization has or what they mean. ADR 0023 decided the first; this ADR decides the second.
otherRelationships as a list
Section titled “otherRelationships as a list”Elsewhere, other<Plural> members are maps keyed by a label, such as otherIds and otherOrgs. otherRelationships is instead a list of the same relationship objects the named lists use. A map would need a label for every entry, but a subtype is optional here, and an entry with only org is valid. Sharing the entry shape also lets one parser handle every list. The exception applies only to otherRelationships; existing other<Plural> maps are unchanged.