> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Graph Model from JSON

> Declare a custom graph model as plain JSON and compile it into a DataPoint model

A minimal guide to defining a custom graph model without writing model classes. Use it when the schema arrives as data — a config file, an API payload, or a document produced by the cognee UI graph-model editor — and you want the same extraction control that a hand-written `DataPoint` model gives you.

## Before You Start

* Complete [Quickstart](/getting-started/quickstart) to understand basic operations
* Ensure you have [LLM Providers](/setup-configuration/llm-providers) configured — the cognify step extracts the graph with an LLM
* Read [Custom Graph Model](/guides/custom-graph-model) for the Python-class equivalent of what the JSON compiles to
* Read [DataPoints](/core-concepts/building-blocks/datapoints) for how graph nodes are identified and indexed

## Code in Action

```python theme={null}
import asyncio

import cognee
from cognee.low_level import graph_model_from_spec

PEOPLE_SPEC = {
    "root": "Person",
    "entities": [
        {
            "name": "Person",
            "description": "A person mentioned in the text.",
            "fields": [
                {
                    "kind": "primitive",
                    "name": "role",
                    "primitive_type": "string",
                    "description": "What the person does.",
                },
                {
                    "kind": "relation",
                    "name": "works_at",
                    "relation": {"target_entity_name": "Organization", "cardinality": "one"},
                },
                {
                    "kind": "relation",
                    "name": "collaborates_with",
                    "relation": {"target_entity_name": "Person", "cardinality": "many"},
                },
            ],
        },
        {
            "name": "Organization",
            "description": "A company, lab, or institution.",
            "fields": [
                {"kind": "primitive", "name": "field_of_work", "primitive_type": "string"},
            ],
        },
    ],
}

TEXT = """
Ada Lovelace worked at the Analytical Engine project alongside Charles Babbage.
Grace Hopper worked at Remington Rand, where she collaborated with the UNIVAC team.
"""


async def main():
    await cognee.forget(everything=True)

    # JSON in, Pydantic model out.
    PeopleGraph = graph_model_from_spec(PEOPLE_SPEC)

    await cognee.add(TEXT, dataset_name="people_from_json")
    await cognee.cognify(datasets=["people_from_json"], graph_model=PeopleGraph)

    results = await cognee.search(
        query_text="Who worked where, and with whom?",
        datasets=["people_from_json"],
    )
    for result in results:
        print(result)


if __name__ == "__main__":
    asyncio.run(main())
```

## What Just Happened

### Step 1: Declare the Schema as JSON

```python theme={null}
PEOPLE_SPEC = {
    "root": "Person",
    "entities": [
        {
            "name": "Person",
            "description": "A person mentioned in the text.",
            "fields": [
                {
                    "kind": "primitive",
                    "name": "role",
                    "primitive_type": "string",
                    "description": "What the person does.",
                },
                {
                    "kind": "relation",
                    "name": "works_at",
                    "relation": {"target_entity_name": "Organization", "cardinality": "one"},
                },
                {
                    "kind": "relation",
                    "name": "collaborates_with",
                    "relation": {"target_entity_name": "Person", "cardinality": "many"},
                },
            ],
        },
        {
            "name": "Organization",
            "description": "A company, lab, or institution.",
            "fields": [
                {"kind": "primitive", "name": "field_of_work", "primitive_type": "string"},
            ],
        },
    ],
}
```

A spec lists the `entities` in the graph and names one of them as the `root` — the entity the generated top-level model is built from (the first entity, when `root` is omitted). Each field is either a `primitive` value, an `enum`, or a `relation` pointing at another declared entity, and a relation's `cardinality` decides whether it holds one target or a list. `collaborates_with` targets `Person` itself, so an entity may relate to its own type. Every `description` is compiled into the schema and onto the generated model's fields, but cognee rebuilds that model into a plain extraction schema before the LLM call and drops them along the way — a `description` documents the spec, it does not steer the LLM. Use [`custom_prompt`](/guides/custom-prompts) for that.

### Step 2: Compile the Spec into a Model

```python theme={null}
# JSON in, Pydantic model out.
PeopleGraph = graph_model_from_spec(PEOPLE_SPEC)
```

`graph_model_from_spec` validates the spec, compiles it to the JSON Schema cognee's model generator accepts, and returns a `DataPoint`-derived Pydantic class. An invalid spec — an unknown relation target, a duplicate entity name, a field colliding with a `DataPoint` infrastructure field — raises `ValidationError` here, before any LLM call is made. `graph_spec_to_json_schema`, also exported from `cognee.low_level`, stops one step earlier and returns that JSON Schema, which is useful for inspecting what the spec compiled into.

### Step 3: Extract with the Generated Model

```python theme={null}
await cognee.add(TEXT, dataset_name="people_from_json")
await cognee.cognify(datasets=["people_from_json"], graph_model=PeopleGraph)
```

The compiled class is an ordinary `graph_model` argument: pass it to `cognify()` (or `remember()`) and it becomes the structured-output schema the LLM must fill, so the graph can only contain the entities and relations the JSON declared.

### Step 4: Search the Resulting Graph

```python theme={null}
results = await cognee.search(
    query_text="Who worked where, and with whom?",
    datasets=["people_from_json"],
)
for result in results:
    print(result)
```

Nothing downstream changes because the model came from JSON — the graph is queried exactly like one built from hand-written classes.

## Advanced Usage

<Accordion title="Node identity and indexing">
  Each entity compiles with a `metadata` default of `index_fields` and `identity_fields`, both defaulting to `["name"]`. `identity_fields` is what makes nodes extracted from different chunks and different runs merge into one graph node when their identity values match, so `Ada Lovelace` mentioned twice stays a single node. Set `"identity_fields": []` on an entity to opt out and give every extracted node a random id.

  ```python theme={null}
  {
      "name": "Person",
      "index_fields": ["name"],
      "identity_fields": [],
      "fields": [...],
  }
  ```

  Both lists may only reference `name` or a declared primitive/enum field — never a relation, and so may `primary_label_field`, a third entity-level key the UI editor writes. `primary_label_field` is validated for that parity but never compiled into the generated model.

  <Note>
    `identity_fields` is a Python-side extension of the DSL. The cognee UI graph-model editor produces the same JSON shape but never emits it, so models built in the frontend do not merge nodes.
  </Note>
</Accordion>

<Accordion title="Field kinds">
  * **`primitive`** — a scalar value, with `primitive_type` one of `"string"` (the default), `"number"`, `"boolean"`, or `"date"` (an ISO date string).
  * **`enum`** — a string restricted to the non-empty `enum_values` list.
  * **`relation`** — an edge named after the field, pointing at `relation.target_entity_name` with `relation.cardinality` of `"one"` or `"many"`.

  `primitive` and `enum` fields accept `"required": true` to force the LLM to supply a value; a relation accepts `required` too, for frontend parity, but it is never compiled. Field names are snake\_case with camelCase aliases accepted (`primitiveType`, `targetEntityName`), so one document works for both the UI editor and Python.
</Accordion>

<Accordion title="Validation limits">
  Validation is also the safety gate — the generated model is built by executing generated code, so names are restricted and size is capped:

  * Entity and field names must be plain identifiers: letters, digits, and underscores, starting with a letter.
  * At most 50 entities per spec, and 40 fields per entity.
  * Entity names must be unique, and may not collide with the `{Name}Type` marker the compiler generates for another entity.
  * Field names must be unique within their entity.
  * `root`, when given, must name a declared entity, and every relation must target one.
  * A declared `name` field must be a string primitive — it is the node's primary identifier.
  * Unknown keys are rejected rather than ignored, so a typo in a key name surfaces immediately.
</Accordion>

<Accordion title="What a custom graph model skips">
  Extraction with any model other than the default `KnowledgeGraph` — whether written in Python or compiled from JSON — bypasses [ontology](/core-concepts/further-concepts/ontologies) grounding and the extra node/edge dedup passes cognee runs on the default path, and `functional_relationships` is not supported here: nothing raises if you pass it, but that pass is built for the default path's entity nodes and will not reliably act on a custom model's graph. Stay on the default path when you need those, and use a custom model when you need a predictable, domain-specific shape instead.
</Accordion>

<Columns cols={3}>
  <Card title="Custom Graph Model" icon="share-2" href="/guides/custom-graph-model">
    Write the same schema as Python DataPoint classes
  </Card>

  <Card title="DataPoints" icon="circle" href="/core-concepts/building-blocks/datapoints">
    How graph nodes are identified, indexed, and merged
  </Card>

  <Card title="Custom Prompts" icon="text-wrap" href="/guides/custom-prompts">
    Tell the LLM what to look for inside the shape you declared
  </Card>
</Columns>
