Skip to content

JSContact

phil/contact

JSContact, RFC 9553, is the JSON form of a contact card that JMAP Contacts (RFC 9610) and recent address books exchange. The library writes a card as JSContact 1.0 and reads one back. The stored form of a card does not change: JSContact is a mapping for import and export.

$document = new JSContactCard('urn:uuid:'.$id, $card, [$address]);

$json = JSContactWriter::toJson($document);       // application/jscontact+json, JSContactCard::MEDIA_TYPE

$read = new JSContactReader()->read($json);
$read->card;        // a ContactCard
$read->addresses;   // the Address values of phil/address
$read->unmapped;    // the properties the library does not map, as they were read
$read->notices;     // what was read but not kept

JSContactCard

PropertyHolds
uidThe card's identifier, 1 to 1024 bytes, required by JSContact
cardThe ContactCard
addressesThe postal addresses, Address values of phil/address, which the card does not hold
unmappedThe properties the library does not map — notes, anniversaries, photos, speakToAs, a vendor's example.com:property —, JSON values as json_decode() gives them without its associative flag, so that an empty object stays one
noticesA JSContactNotice for each part read but not kept: its JSON pointer, /phones/p1/contexts, and the reason

The unmapped properties are written back by JSContactWriter, after the mapped ones; they never hold a property the writer writes itself, JSContactCard::MAPPED, or InvalidContact is thrown. An application that keeps them stores them beside the card.

The mapping

JSContactCardWrittenRead
uidJSContactCard::$uidAlwaysRequired
kindPerson or organisationindividual, or org for a card that names only its organisationorg makes the card name its organisation; any kind other than individual and org is read as a person's, with a notice
languageThe culture of the structured nameFrom the culture, else from the unmapped languageThe culture of the structured name; kept unmapped when no structured name takes it
namename and structuredNamefull, the components in display order, isOrdered, defaultSeparator, and sortAs.surname when the culture sorts the name otherwise than it is writtenSee below
organizationsorganizationOne entryThe first entry's name
titlestitleOne entryThe first entry of kind title
emailsemailsOne entry eachEach address
phonesphonesnumber as a tel: address, and the feature of the roleEach number, its role from features
onlineServiceshandles, or urlsservice, the enum case's name, user, and uri when the service has a linkA handle when service names an accepted messaging service, else a web address when uri is one of an accepted kind
linksurlsOne entry eachEach uri, its kind detected
addressesJSContactCard::$addressescomponents and countryCodeSee below

Entries are written under the keys o1, t1, e1, p1, s1, l1 and a1, numbered from 1.

Names

The components title, given, given2, surname, surname2, generation and credential are the honorific, the given name, the additional names, the surname, the second family name, the generation and the credential. A particle is written with its surname, de Gaulle, and sortAs.surname is the sort form, Gaulle; on reading, the part of the surname before its sort form, ending with a space or an apostrophe, is the particle. A particle the culture keeps in the sort key is therefore read back as part of the family name, which writes and sorts the name the same way.

With isOrdered, a surname before the given name reads as NameOrder::FamilyFirst, kept only when it is not the order of the culture. A notice reports a component of another kind, a second component of a kind kept once, an additional name beyond the tenth, a separator — separators follow the convention of the name —, a sortAs or a defaultSeparator the culture does not give, a language the name cannot take as its culture, and components that make no name. The card of an organisation keeps full and reports the components.

Phone lines

RoleFeature writtenRead from
Mobilemobilemobile, with voice and text implied
Faxfaxfax
Pagerpagerpager
Voicevoicevoice, or no feature
Texttexttext

A line takes the role of its first feature in the order of the table; a notice reports each other feature, video, textphone or main-number among them. A number without its country code is read in the reader's region.

Addresses

The writer writes each address line as a name component, the dependent locality as district, the locality as locality, the administrative area as region, and the postal code then the sorting code each as postcode. The recipient fields of an address are left out: the card names its owner.

The reader writes the street components — number, name, direction, block, building, floor, apartment, room, postOfficeBox and landmark — on the address lines, in their order: a name after a line that already has one starts the next line, and an address keeps three lines. The first district, locality and region are kept, the first postcode is the postal code and the second the sorting code. A notice reports any other component, an address without its countryCode, and an address phil/address refuses.

The reader

$reader = new JSContactReader(
    maxBytes: 65_536,
    maxDepth: 16,
    maxEntries: 100,
    region: 'CA',
    messengers: [EnumSet::all(KnownMessenger::class), EnumSet::all(MyMessenger::class)],
    urlTypes: [EnumSet::all(KnownUrlType::class)],
);
ArgumentDefaultMeans
maxBytes65 536The longest text read
maxDepth16The deepest nesting of its JSON
maxEntries100The most entries of one map or list
regionnullThe country a phone number without its country code is read in
messengersEvery KnownMessengerThe messaging services an online service is read as
urlTypesEvery KnownUrlTypeThe kinds of web address accepted

The reader is strict. UnreadableJSContact is thrown for a text beyond the bounds, no JSON, no Card of version 1.0, no uid, a property of the wrong JSON type, an entry key that is no JSContact Id, an entry whose @type is not the expected one, an entry without its required text, and a card the library refuses, a name longer than 255 characters for one. A value its type refuses — an email that is none, a number no plan knows, a web address of no accepted kind — leaves its entry out with a notice. Nothing is dropped silently: a property is mapped, kept unmapped, or reported.

The reader holds no state between two cards: one instance serves every request of a worker.