Doctrine JSON ODM: storing PHP object graphs in a relational database
An object document mapper for Doctrine ORM using JSON types of modern RDBMS.
At a glance
- What is it?
- Doctrine JSON ODM lets a Doctrine entity keep a property of arbitrary PHP objects in a JSON column of PostgreSQL or MySQL, serialised with the Symfony Serializer and hydrated back on read. It gives you hybrid models where normalised columns and schema-less documents sit side by side, and the price is that queries against the JSON part are database-specific and indexes have to be declared deliberately.
- Who is it for?
- Doctrine JSON ODM suits a PHP team that wants to keep a variable payload, a document of third-party data or a nested structure inside an otherwise relational Doctrine model, and that is already using the Symfony Serializer. It is the wrong tool if you need to query that payload relationally across many records, because the README points at the database's own JSON facilities and indexes rather than at a portable query layer, and those differ between PostgreSQL and MySQL.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 16 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Hybrid models: relational columns next to documents
Doctrine JSON ODM is an object document mapper for Doctrine ORM that leverages the JSON types of modern relational databases. The pitch in the README is a question about whether you ever wanted a tool creating powerful data models mixing traditional, efficient relational mappings with modern schema-less and NoSQL-like ones. That is the use case, and it is narrower than it first sounds. It is not a replacement for a document database, and it is not a general purpose serialisation layer for the whole entity. It is a way to keep one property of an entity, or one embedded value, as a free-form document while the rest of the entity stays in properly typed columns. The audience is a PHP team that already uses Doctrine ORM and has hit the wall of modelling a variable structure as a table with a nullable column per possible key. The database does the storage and the indexing, PostgreSQL and MySQL both, and the project links the documentation of the JSON type capabilities as the place where the real capability lives.
A custom Doctrine type backed by the Symfony Serializer
The mechanism is a Doctrine type. Doctrine JSON ODM provides a json_document column type for properties of Doctrine entities, and starting with Doctrine DBAL 4.3.0 a jsonb_document type is also available, leveraging native JSONB support. The content of a property mapped with this type is serialised in JSON using the Symfony Serializer and stored in a dynamic JSON column in the database. On the way back, the JSON content of that column is transformed to its original values, thanks again to the Symfony Serializer, and the README claims that all PHP objects and structures will be preserved. What that means in practice is that you can store any type of serialisable PHP data structure in a property mapped with the json_document type, including arrays, objects and nested objects. The dependency on the Symfony Serializer is a real design decision rather than an implementation detail, because it means the shape of the stored document is governed by the normalisers you configure, and the same serializer instance is what both encodes and decodes it.
Registering the type and creating the entity manager
If you use Symfony or API Platform, the README says you do not need to do anything else, because the integration registers the type for you. If you use Doctrine directly, you register the type yourself and attach a serializer to it, in a bootstrap that looks like this:
if (!Type::hasType('json_document')) {
Type::addType('json_document', JsonDocumentType::class);
Type::getType('json_document')->setSerializer($serializer);
}
// jsonb_document requires Doctrine DBAL 4.3.0+
if (class_exists(JsonbType::class) && !Type::hasType('jsonb_document')) {
Type::addType('jsonb_document', JsonbDocumentType::class);
Type::getType('jsonb_document')->setSerializer($serializer);
}The serializer you attach is a Symfony Serializer built from a list of normalisers, which the README's full bootstrap shows as a BackedEnumNormalizer, a UidNormalizer, a DateTimeNormalizer, an ArrayDenormalizer and an ObjectNormalizer, with a JsonEncoder for output. That list is the effective contract of what round trips. Enums, identifiers, dates, arrays and plain objects each need a normaliser to survive the trip, and if a type you use is not covered by one of them, the document will not come back the way it went in. The double registration pattern is also defensive, checking whether the type already exists and whether the DBAL class is available, so the same bootstrap can serve installations that do and do not have the newer JSONB type.
Storing a graph and reading it back
The usage example is deliberately plain. You declare a normal Doctrine entity with a normal identifier and a normal name column, and add one property with the JSON type, which can contain anything, an array, objects, nested objects. The objects you put inside do not have to be entities. The README is explicit about this, calling the second and third classes POPOs, plain old PHP objects, and noting that they can contain anything, with a title and a float in one case and a name and an int in the other. Assigning them to the JSON property and persisting the parent is all it takes:
$bar = new Bar();
$bar->title = 'Bar';
$bar->weight = 12.3;
$baz = new Baz();
$baz->name = 'Baz';
$baz->size = 7;
$foo = new Foo();
$foo->name = 'Foo';
$foo->misc = [$bar, $baz];
$entityManager->persist($foo);
$entityManager->flush();And reading the entity back gives you the same graph, which the README demonstrates by dumping the property and noting it is the same as what was set earlier. The entity itself is still a first class citizen, so it participates in normal Doctrine behaviour, identity map, queries, cascading, and the rest. What is stored in the column is a document, and what you get back is objects.
Type aliases keep the database stable across refactors
The feature that matters for a long-lived project is type aliasing. By default the serialiser writes the fully qualified class name into the document, and the README explains why you would rather not. Using custom type aliases as the type marker has a couple of benefits. If you move or rename your document classes, you can update your type map without migrating database content, which turns a breaking namespace change into a configuration change. And for applications that might store millions of records with JSON documents, it can also save some storage space, because foo is a lot cheaper than a fully qualified class name repeated in every row. The mapping itself is configuration, in the Symfony case a bundle configuration file:
dunglas_doctrine_json_odm:
type_map:
foo: App\Something\Foo
bar: App\SomethingElse\BarWith that in place the object is stored with a short marker:
{ "#type": "foo", "someProperty": "someValue" }You can introduce aliases at any point in time, and the README notes that already persisted documents containing class names will still deserialise correctly, so the change can be made incrementally on a live database. For a project that does not use the Symfony bundle there is a second option, your own custom type mapper implementing the library's TypeMapper interface.
Which databases, and which versions
The supported storage is specific. The library works with the JSON and JSONB columns of PostgreSQL, from version 9.4 and later, and with the JSON column type of MySQL from 5.7.8 and later. That version floor is the point at which those databases gained the native JSON types and the indexing options that make the whole approach worth it, and the README links the PostgreSQL documentation for the type and for the JSON indexing section, since you can define indexes for those documents. The performance claim in the README is that querying schema-less documents is easy, powerful and fast, with a link to a comparison article arguing that PostgreSQL outperforms MongoDB. Treat that as a pointer to an argument rather than a benchmark, since the project documentation does not include a measurement of its own. What the project does document is the capability it relies on, which is that the database can index inside the document, and that is the part to evaluate against your own queries.
Project shape, versions and licence
The repository is a conventional PHP library, with a composer.json at the root, the source under src/, the test suite under tests/ and a phpunit.xml.dist for the test configuration, plus a CONTRIBUTING.md, a SECURITY.md for reporting problems and the MIT licence in a LICENSE file. The homepage is the author's site at dunglas.fr, which is where the same person keeps the other libraries in the API Platform and Symfony ecosystem. The release history is steady, with v1.5.1 published on 2026-10-07, v1.5.0 in February 2026 and v1.4.2 in November 2024, and the last push to the main branch was on 2026-10-07 as well, so the branch and the newest tag are in step. The continuous integration badge is for a tests workflow, and the project also carries Scrutinizer and StyleCI badges, which are the conventional linting and static analysis services for the PHP world. The concept behind the library was presented by Benjamin Eberlei at Symfony Catalunya in 2016, and the README points to that talk for the ideas rather than the code.
Editorial conclusion
Doctrine JSON ODM suits a PHP team that wants to keep a variable payload, a document of third-party data or a nested structure inside an otherwise relational Doctrine model, and that is already using the Symfony Serializer. It is the wrong tool if you need to query that payload relationally across many records, because the README points at the database's own JSON facilities and indexes rather than at a portable query layer, and those differ between PostgreSQL and MySQL. Before adopting it, check the database version floor, since it needs PostgreSQL 9.4 or later and MySQL 5.7.8 or later, and register the type explicitly when you bootstrap Doctrine by hand, since the Symfony and API Platform integrations do that for you.
Frequently asked questions
How do I install Doctrine JSON ODM?
Use Composer with composer require dunglas/doctrine-json-odm. If you are on Symfony or API Platform you need to do nothing else, because the integration registers the type for you, while a direct Doctrine setup needs a bootstrap that registers the json_document type and attaches a serializer to it.
Which databases and versions are supported?
The JSON and JSONB column types of PostgreSQL from 9.4 and later, and the JSON column type of MySQL from 5.7.8 and later. A jsonb_document type backed by native JSONB support is available from Doctrine DBAL 4.3.0.
What can I put in a json_document property?
Any type of serialisable PHP data structure, including arrays, objects and nested objects. The contained objects do not have to be Doctrine entities, and the README calls them plain old PHP objects, so a property of that type can hold a graph of ordinary classes that is hydrated back into the same objects on read.
What are type aliases for, and how do I set them up?
They let the serialiser write a short alias instead of a fully qualified class name. That way renaming or moving a document class needs a type map update rather than a data migration, and it saves storage on applications with millions of JSON documents. In Symfony you configure them in the bundle configuration under dunglas_doctrine_json_odm and type_map, and documents stored before the change still deserialise correctly.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dunglas-doctrine-json-odm)Was this useful?