# JSON-LD — Quickstart: Zero to First Transform: Running a JSON-LD Vocabulary Through CoreModels

By the end of this page you will have made exactly one HTTP call and produced two artifacts: a JSON Schema generated from an RDF vocabulary, and a lossiness ledger that states - in writing - what the conversion could not carry across. Learning to read both is the entire skill. Everything else in the CoreModels transform surface is a variation on this loop.

# Zero to First Transform: Running a JSON-LD Vocabulary Through CoreModels

By the end of this page you will have made exactly one HTTP call and produced two artifacts: a
JSON Schema generated from an RDF vocabulary, and a lossiness ledger that states - in writing -
what the conversion could not carry across. Learning to read both is the entire skill. Everything
else in the CoreModels transform surface is a variation on this loop.

We are the CoreModels team at ARAMAI. The inputs and outputs below are real: we ran them through
the shipped coders and pasted the results.

## What you need

Three things, no installation:

- A CoreModels host. We write `https://coremodels.example.com` throughout - substitute yours.
- A bearer token in `$TOKEN`, and a project id in `$PROJECT_ID`. The call we make is stateless
  and needs only **Viewer** access; the project scopes authorization and nothing else.
- `curl` and `jq`. The `jq` part matters more than it looks: schema documents travel as JSON
  **strings** inside the request body, and hand-escaping a JSON-LD file is where first attempts
  usually die.

## The vocabulary

Save this as `person.jsonld`. It is small but not a toy: two classes with an inheritance edge,
three properties, two literal datatypes, one object-valued property, and one term borrowed from
schema.org rather than minted locally.

```json
{
  "@context": {
    "schema": "https://schema.org/",
    "xsd": "http://www.w3.org/2001/XMLSchema#",
    "ex": "https://example.org/"
  },
  "@graph": [
    { "@id": "ex:Person", "@type": "rdfs:Class", "rdfs:label": "Person" },
    { "@id": "ex:Employee", "@type": "rdfs:Class", "rdfs:label": "Employee",
      "rdfs:subClassOf": { "@id": "ex:Person" } },
    { "@id": "schema:name", "@type": "rdf:Property", "rdfs:label": "name",
      "schema:domainIncludes": { "@id": "ex:Person" },
      "schema:rangeIncludes": { "@id": "xsd:string" } },
    { "@id": "ex:age", "@type": "rdf:Property", "rdfs:label": "age",
      "schema:domainIncludes": { "@id": "ex:Person" },
      "schema:rangeIncludes": { "@id": "xsd:integer" } },
    { "@id": "ex:manager", "@type": "rdf:Property", "rdfs:label": "manager",
      "schema:domainIncludes": { "@id": "ex:Employee" },
      "schema:rangeIncludes": { "@id": "ex:Person" } }
  ]
}
```

That is the dialect the `jsonld` format key reads: a vocabulary, published the way schema.org
publishes its terms. Nodes typed `rdfs:Class` are the types. Nodes typed `rdf:Property` are the
fields, attached to their class by `schema:domainIncludes` and typed by `schema:rangeIncludes`.
`rdfs:subClassOf` is inheritance. Four prefixes are always understood without being declared -
`rdf`, `rdfs`, `xsd`, and `schema` - which is why `rdfs:Class` works above even though the
`@context` never mentions `rdfs`.

## The one call

We use `schema/map`, the stateless mapping endpoint. It runs the full engine - decode the source,
produce a plan, validate the plan through the engine's universal gate, execute it, encode the
target - and returns everything it did.

One thing will bite you if nobody warns you first: the default `inferred` strategy matches the
source against a **target hint**, so it needs something to aim at. Omit the hint and the call
comes back with `The inference resolver requires a target IR to match against.` For a
straight format conversion the idiom is to hand the document to itself as the hint. Every
construct then matches its own twin and the plan comes out as clean identity mappings.

```bash
jq -n --rawfile doc person.jsonld '{
  sourceFormat: "jsonld",
  sourceSchema: $doc,
  targetFormat: "jsonschema",
  targetHintFormat: "jsonld",
  targetHintSchema: $doc,
  mapping: { kind: "inferred" }
}' > body.json

curl -s -X POST "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @body.json
```

Two headers, one body, no setup, no state. `schema/map` never writes to the project - every call
is inherently a dry run.

## The schema that comes back

The response uses the envelope shared by every transform endpoint: `success`, `lossiness`,
`errors`, plus this verb's payload - `schema` and `plan`. For JSON-shaped targets, `schema` is a
JSON object, not a string. Here it is, exactly as the engine produced it:

```json
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "x-maps-to": { "schema": "https://schema.org/name" }
    },
    "age": {
      "type": "integer",
      "x-maps-to": { "ex": "https://example.org/age" }
    }
  },
  "x-maps-to": { "ex": "https://example.org/Person" },
  "$defs": {
    "ex:Employee": {
      "allOf": [
        { "$ref": "#/$defs/ex:Person" },
        {
          "type": "object",
          "properties": {
            "manager": {
              "$ref": "#/$defs/ex:Person",
              "x-maps-to": { "ex": "https://example.org/manager" }
            }
          }
        }
      ],
      "x-maps-to": { "ex": "https://example.org/Employee" }
    }
  }
}
```

Four things happened there, and together they are how CoreModels treats JSON-LD.

**The IRIs came through.** In a vocabulary, a node's `@id` *is* a global identifier, so the coder
expands each qname against the `@context` and lifts it into a cross-standard mapping annotation -
the `x-maps-to` entries you see on the root object and on every property. `schema:name` became
`https://schema.org/name`; `ex:age` became `https://example.org/age`. Push this schema onward to
Avro, LinkML, SQL, or OWL later and those identities travel with it. That is why we treat JSON-LD
as a carrier of meaning rather than one more syntax.

**Inheritance became `allOf`.** `ex:Employee rdfs:subClassOf ex:Person` is expressed the way JSON
Schema expresses extension: an `allOf` over a `$ref` to the parent plus the subclass's own
properties.

**Datatypes landed cleanly.** `xsd:string` became `"type": "string"`, `xsd:integer` became
`"type": "integer"`, and the object-valued range on `manager` became a real `$ref` - a reference,
not a stringified type name.

**Nothing is required.** There is no `required` array, and that is faithful reading, not an
omission. RDF is open-world: an `rdf:Property` never asserts that instances must supply a value,
so every decoded property arrives optional. If your downstream contract needs mandatory fields,
they have to come from somewhere that can state them.

## The ledger

`lossiness` is the honest change report, and the habit we ask everyone to build is: read it every
time. Each entry carries a `kind` - `StructuralDrop` (no home in the target), `TypeApproximation`
(close but not exact), `ConstraintRelaxation` (a rule could not be enforced), or
`SemanticNarrowing` (meaning was narrowed or guessed) - plus a `path` naming the construct and a
plain-English `explanation`.

For this conversion the ledger is empty:

```json
{ "lossiness": [] }
```

Empty means the target held everything the source said: classes to objects, properties to typed
properties, subclassing to `allOf`, IRIs to `x-maps-to`. Nothing to review.

The rule that empty ledgers can hide is the one worth memorizing: **`success: true` means "it
ran", not "nothing changed."** Aim the same vocabulary at a relational target and the ledger stops
being empty - an enumeration has no inline representation in Postgres, and the engine says so
rather than letting you find out in production. Entries are review items, not errors; a
non-empty ledger on a successful call is by design.

## The plan, in one paragraph

The response's third artifact is `plan` - the operations the engine actually executed, each
tagged with its origin. For this call it is five operations: two type mappings and three element
mappings, all `"origin": "Inferred"`. Store it and you can replay the identical transformation
later through the `plan/execute` endpoint; the same plan against the same source produces the
same output. That is what turns a one-off conversion into something a build can repeat, and it is
a subject that deserves its own article.

## The other direction, in two lines

If the model you care about already lives in a CoreModels project, the mirror-image call publishes
it *as* a vocabulary. Same envelope, same ledger discipline, Viewer role, and the smallest body on
the whole API:

```bash
curl -s -X POST "https://coremodels.example.com/graph/transform/schema/export/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "format": "jsonld" }'
```

Types come back as `rdfs:Class` nodes, inheritance as `rdfs:subClassOf`, elements as
`rdf:Property` nodes wired by `schema:domainIncludes` and `schema:rangeIncludes`, and controlled
lists as classes subclassing `schema:Enumeration` with one node per term. Because `jsonld` is a
JSON-shaped format, the `schema` field is a JSON object here too.

## What to try next

You have run the loop end to end: JSON-LD in, JSON Schema out, ledger read. The same call works in
every direction the engine supports - swap `targetFormat` for `linkml`, `avro`, `owl`, `shex`,
`odcs`, `protobuf`, or `sql` (with `"vendor": "postgres" | "mysql" | "sqlserver"`) and the shape of
the response never changes. And because the format decodes as well as encodes, you can write a
vocabulary straight into a project with `schema/import` when you want it governed rather than just
converted.

For the full endpoint reference and ready-to-paste request bodies for every format key, see the
Schema Transformation API guide in the CoreModels docs.
