Skip to content

Mapping Examples ​

Each example on this page is a complete Portable Data Module (PDM). Each example shows sample input and the output that Precept returns. For the syntax, see Mapping Engine and PDM Language.

The tabs under each example show the data that Precept reads (Input) and the data that Precept returns (Output). The model name in each tab title is the model of that data.

We ran each example on this page through the Precept mapping engine.

Rename fields ​

Use <-> to copy a field to a field with a different name. The rule works in both directions.

pdm
vendor acme
name rename

model Contact {
  first_name: string
  last_name: string
  date_of_birth: string
}

model Person {
  firstName: string
  lastName: string
  dob: string
}

mapping Contact <-> Person {
  first_name <-> firstName
  last_name <-> lastName
  date_of_birth <-> dob
}
json
{ "first_name": "Jane", "last_name": "Smith", "date_of_birth": "1985-05-15" }
json
{ "firstName": "Jane", "lastName": "Smith", "dob": "1985-05-15" }
json
{ "firstName": "John", "lastName": "Doe", "dob": "1990-01-01" }
json
{ "first_name": "John", "last_name": "Doe", "date_of_birth": "1990-01-01" }

Map nested fields ​

Use a dotted path to read or write a field inside an object. Precept creates the object when it writes.

pdm
vendor acme
name nested

model FullName {
  first: string
  last: string
}

model Contact {
  name: FullName
}

model Person {
  firstName: string
  lastName: string
}

mapping Contact <-> Person {
  name.first <-> firstName
  name.last <-> lastName
}
json
{ "firstName": "John", "lastName": "Doe" }
json
{ "name": { "first": "John", "last": "Doe" } }
json
{ "name": { "first": "Jane", "last": "Smith" } }
json
{ "firstName": "Jane", "lastName": "Smith" }

Nest and flatten an object ​

Use a field list on the right side to group top-level fields into one object.

pdm
vendor acme
name geo

model Coordinates {
  lat: number
  lon: number
}

model Contact {
  coordinates: Coordinates
}

model Person {
  lat: number
  lon: number
}

mapping Contact <-> Person {
  coordinates <-> {lat, lon}
}
json
{ "lat": 42.3601, "lon": -71.0589 }
json
{ "coordinates": { "lat": 42.3601, "lon": -71.0589 } }
json
{ "coordinates": { "lat": 42.3601, "lon": -71.0589 } }
json
{ "lat": 42.3601, "lon": -71.0589 }

Skip empty optional fields ​

Put ? after a field to skip the rule when the source value is null or missing. Without ?, Precept writes null.

pdm
vendor acme
name optional

model Contact {
  maiden_name?: string
}

model Person {
  previousName?: string
}

mapping Contact <-> Person {
  maiden_name? <-> previousName
}
json
{ "previousName": "Smith" }
json
{ "maiden_name": "Smith" }
json
{ "previousName": null }
json
{}

Translate values with match ​

Use a match block to translate a set of values. The _ pair gives the value for all other input.

pdm
vendor acme
name status

model Contact {
  status: string
}

model Person {
  is_active: boolean
}

mapping Contact <-> Person {
  status <- match {
    "Active" <-> true
    "Inactive" <-> false
    _ <-> null
  } -> is_active
}
json
{ "status": "Active" }
json
{ "is_active": true }
json
{ "status": "Pending" }
json
{ "is_active": null }
json
{ "is_active": false }
json
{ "status": "Inactive" }

Split and join a name with separator ​

separator with a field list splits one string into an object. In the other direction, it joins the fields into one string.

pdm
vendor acme
name names

model Contact {
  fullName: string
}

model Name {
  first: string
  last: string
}

model Person {
  name: Name
}

mapping Contact <-> Person {
  fullName <- separator(" ") | {first, last} -> name
}
json
{ "fullName": "Jane Smith" }
json
{ "name": { "first": "Jane", "last": "Smith" } }
json
{ "name": { "first": "John", "last": "Doe" } }
json
{ "fullName": "John Doe" }

If the string has fewer parts than the field list, the remaining fields are null:

json
{ "fullName": "Cher" }
json
{ "name": { "first": "Cher", "last": null } }

Split an address into top-level fields ​

If the rule has no target field after the field list, Precept reads and writes the fields at the top level of the right model.

pdm
vendor acme
name address

model Contact {
  address_line: string
}

model Address {
  street: string
  city: string
  state: string
  zip: string
}

mapping Contact <-> Address {
  address_line <- separator(", ") | {street, city, state, zip}
}
json
{ "address_line": "1 Main St, Boston, MA, 02110" }
json
{ "street": "1 Main St", "city": "Boston", "state": "MA", "zip": "02110" }
json
{ "street": "1 Main St", "city": "Boston", "state": "MA", "zip": "02110" }
json
{ "address_line": "1 Main St, Boston, MA, 02110" }

Select a fallback value with coalesce ​

coalesce writes the first value that is not empty, or the default.

pdm
vendor acme
name display

model Contact {
  displayName: string
}

model Person {
  fullName: string
  email: string
}

mapping Contact <-> Person {
  displayName <- coalesce(fullName, email, "Unknown")
}
json
{ "fullName": "Alice Smith", "email": "alice@example.com" }
json
{ "displayName": "Alice Smith" }
json
{ "fullName": "", "email": "bob@example.com" }
json
{ "displayName": "bob@example.com" }
json
{}
json
{ "displayName": "Unknown" }

In the other direction, a value that contains @ goes to the second field. Another value goes to the first field.

json
{ "displayName": "user@example.com" }
json
{ "email": "user@example.com" }
json
{ "displayName": "John Doe" }
json
{ "fullName": "John Doe" }

Parse a phone number with template ​

template extracts named parts from a structured string. Put the raw string on the right side of the rule.

pdm
vendor acme
name phones

model PhoneParts {
  countryCode: string
  areaCode: string
  localNumber: string
}

model Contact {
  parts: PhoneParts
}

model PhoneRecord {
  phone: string
}

mapping Contact <-> PhoneRecord {
  parts <- template(`{countryCode:num(1-3)}-{areaCode:num(3)}-{localNumber:num(4)}`) -> phone
}
json
{ "phone": "44-555-1234" }
json
{ "parts": { "countryCode": "44", "areaCode": "555", "localNumber": "1234" } }
json
{ "parts": { "countryCode": "1", "areaCode": "212", "localNumber": "5550" } }
json
{ "phone": "1-212-5550" }

Read fixed-width fields with slice ​

slice reads a part of a string by position. This example reads a security identifier and a code from one packed column. Put the model with the packed column on the right side of the mapping header.

pdm
vendor acme
name positions

model Parsed {
  cusip: string
  code: string
  amount: number
}

model RawFile {
  packed_data: string
  amount: number
}

mapping Parsed <-> RawFile {
  packed_data | slice(1, 9) <-> cusip
  packed_data | slice(10, 5) <-> code
  amount <-> amount
}
json
{ "packed_data": "IBM123456ABCDE", "amount": 500 }
json
{ "cusip": "IBM123456", "code": "ABCDE", "amount": 500 }

slice removes spaces from the start and end of each part:

json
{ "packed_data": "IBM      Hello", "amount": 100 }
json
{ "cusip": "IBM", "code": "Hello", "amount": 100 }

In data ingestion, Precept compiles the same rules to SQL:

sql
SELECT "amount", TRIM(SUBSTR("packed_data", 1, 9)) AS "cusip", TRIM(SUBSTR("packed_data", 10, 5)) AS "code"

Chain functions in a pipeline ​

A pipeline is a list of functions with | between them. Precept gives the result of each function to the next function. This example uses two list functions:

  • head reads the first item of a list.
  • tail reads the last item of a list.

The grid field is a list of lists. head | tail reads the first inner list, and then the last item of that list.

pdm
vendor acme
name grid

model Cell {
  cell: string
}

model Grid {
  grid: string[][]
}

mapping Cell <-> Grid {
  cell | head | tail <-> grid
}
json
{ "grid": [["a", "b"], ["c"]] }
json
{ "cell": "b" }

In the other direction, each function writes its value back as a list, in the opposite order. The result is a list that holds one list:

json
{ "cell": "z" }
json
{ "grid": [["z"]] }

Run a function on one side with sort_by ​

A pipeline is attached to the field next to it. In ranked | sort_by(priority) <-> queue, the pipeline is attached to ranked.

Some functions work in two directions: head, tail, slice, separator, coalesce and template. Precept runs these functions in both directions. Other functions, for example sort_by, work in one direction only. Precept runs a one-way function only when it writes the field that the function is attached to.

sort_by(key) sorts a list of records by the field key. Records with the same key stay in their original order. Records with a null key go to the end.

pdm
vendor acme
name queue

model Assignee {
  name: string
  priority: number
}

model Ranked {
  ranked: Assignee[]
}

model Queue {
  queue: Assignee[]
}

mapping Ranked <-> Queue {
  ranked | sort_by(priority) <-> queue
}

When Precept writes ranked, it sorts the list:

json
{
  "queue": [
    { "name": "A", "priority": 2 },
    { "name": "B", "priority": 1 },
    { "name": "C", "priority": 1 },
    { "name": "D", "priority": null }
  ]
}
json
{
  "ranked": [
    { "name": "B", "priority": 1 },
    { "name": "C", "priority": 1 },
    { "name": "A", "priority": 2 },
    { "name": "D", "priority": null }
  ]
}

When Precept writes queue, sort_by does not run. Precept copies the list without changes.

Attach a function to the right field ​

To attach a pipeline to the field on the right side, put the pipeline after <-> and before the field: code <-> slice(3, 4) | packed. This rule reads characters 3 to 6 of packed.

pdm
vendor acme
name parts

model Part {
  code: string
}

model Line {
  packed: string
}

mapping Part <-> Line {
  code <-> slice(3, 4) | packed
}
json
{ "packed": "xxABCDyy" }
json
{ "code": "ABCD" }

In the other direction, slice pads the value with spaces to 4 characters and puts it at position 3:

json
{ "code": "AB" }
json
{ "packed": "  AB  " }

Attach functions to both fields ​

A rule can have a pipeline on each side. In this example, the old model has one assignee and the new model has a list of assignees. head is attached to assignee, and sort_by is attached to assignees.

head("clear") sets what Precept writes when the value is null. With "clear", Precept writes an empty list.

pdm
vendor acme
name tracker

model Assignee {
  name: string
  priority: number
}

model Old {
  assignee: Assignee
}

model New {
  assignees: Assignee[]
}

mapping Old <-> New {
  assignee | head("clear") <-> sort_by(priority) | assignees
}

When Precept reads New data, head reads the first assignee. sort_by is attached to assignees, so it does not run in this direction. The result is Bob, not Alice:

json
{
  "assignees": [
    { "name": "Bob", "priority": 2 },
    { "name": "Alice", "priority": 1 }
  ]
}
json
{ "assignee": { "name": "Bob", "priority": 2 } }

When Precept reads Old data, head writes the assignee back as a list with one item. Then sort_by sorts the list:

json
{ "assignee": { "name": "Alice", "priority": 1 } }
json
{ "assignees": [{ "name": "Alice", "priority": 1 }] }

If assignee is null, the output is { "assignees": [] }.

Convert a record with mapFn ​

A mapFn holds JavaScript. Use it when the declarative rules cannot express the conversion. A mapFn converts data in one direction only.

pdm
vendor acme
name users

model User {
  id: string
  name: string
  email: string
  active: boolean
}

model Person {
  firstName: string
  lastName: string
  email: string
}

mapFn (user: User): Person {=
  const [firstName, ...rest] = user.name.split(' ')

  return {
    firstName: firstName || '',
    lastName: rest.join(' '),
    email: user.email
  }
=}
json
{ "id": "u-1", "name": "Ann Marie Cole", "email": "ann@example.com", "active": true }
json
{ "firstName": "Ann", "lastName": "Marie Cole", "email": "ann@example.com" }

Convert a list with mapFn ​

A mapping block converts one record. If an action returns an array, use a mapFn that maps each item. Precept passes the full action result to the mapFn.

pdm
vendor acme
name contacts

model Contact {
  first_name: string
  last_name: string
}

model Person {
  firstName: string
  lastName: string
}

mapFn (contacts: Contact): Person[] {=
  return contacts.map((c) => ({
    firstName: c.first_name,
    lastName: c.last_name
  }))
=}
json
[
  { "first_name": "Jane", "last_name": "Smith" },
  { "first_name": "John", "last_name": "Doe" }
]
json
[
  { "firstName": "Jane", "lastName": "Smith" },
  { "firstName": "John", "lastName": "Doe" }
]

A complete module ​

This module connects to a REST API, defines two models, and maps one model to the other. Precept calls GET https://api.acme.example/contacts/{id} when you run the get action.

pdm
vendor acme
name crm
version "1.0"

meta {
  productName: "Acme CRM"
  description: "Contacts from the Acme CRM API"
}

defaults {
  http {
    baseUrl: "https://api.acme.example"
    headers: {
      Authorization: `Bearer {config.apiKey}`
    }
  }
}

config {
  apiKey: secret string
}

model Contact {
  id: string primary
  first_name: string
  last_name: string
  email?: string format("email")
  status: "Active" | "Inactive"

  resource {
    query get(id: string): Contact -> {
      http.get(`/contacts/{id}`)
    }
  }
}

model Person {
  contactId: string
  firstName: string
  lastName: string
  email?: string
  isActive: boolean
}

mapping Contact <-> Person {
  id <-> contactId
  first_name <-> firstName
  last_name <-> lastName
  email? <-> email
  status <- match {
    "Active" <-> true
    "Inactive" <-> false
  } -> isActive
}
json
{ "id": "c-42", "first_name": "Jane", "last_name": "Smith", "email": "jane@example.com", "status": "Active" }
json
{ "contactId": "c-42", "firstName": "Jane", "lastName": "Smith", "email": "jane@example.com", "isActive": true }

The match block has no _ pair. If no pair matches, Precept does not write isActive:

json
{ "id": "c-43", "first_name": "John", "last_name": "Doe", "status": "Pending" }
json
{ "contactId": "c-43", "firstName": "John", "lastName": "Doe" }

To get the result as a Person, send this request:

http
GET /rest/acme/crm/resource/Contact/get?id=c-42
Accept: application/json; precept-mapping-target="acme/crm.Person"