diff --git a/composer.json b/composer.json
index 97605266d4f9562a8e4d67ddbf529c1445ec62a9..6cfb74e8e9ca35c4e01b857f0429ec54369b3ab6 100644
--- a/composer.json
+++ b/composer.json
@@ -20,6 +20,7 @@
         "ext-json": "*",
         "ext-zip": "*",
         "api-platform/core": "^2.2",
+        "beberlei/assert": "^3.2",
         "doctrine/cache": "^1.8",
         "doctrine/inflector": "^1.3",
         "psr/container": "^1.0",
@@ -34,6 +35,7 @@
     "require-dev": {
         "irstea/php-cs-fixer-config": "^1.0",
         "irstea/phpmd-config": "^1.0.0",
+        "irstea/phpstan-config": "^1.0",
         "jakub-onderka/php-parallel-lint": "^1.0",
         "maglnet/composer-require-checker": "^1.0",
         "phploc/phploc": "^4.0",
diff --git a/phpstan.neon b/phpstan.neon
index d87786cb5e62cecd973f4bb43500496078153c46..e752a17312ed0261d3170e4a7f4776151fcfdf93 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,5 +1,7 @@
+includes:
+    - vendor/irstea/phpstan-config/strict.neon
+
 parameters:
-    level: 7
     paths:
         - src/php
 
diff --git a/src/php/Command/NgModelGenerateCommand.php b/src/php/Command/NgModelGenerateCommand.php
index ffe83afb87d241084f253a9da2df92df01a73fcc..8be47ec5b322c82214a2d4ef231d63dce593cf21 100644
--- a/src/php/Command/NgModelGenerateCommand.php
+++ b/src/php/Command/NgModelGenerateCommand.php
@@ -22,6 +22,7 @@
 namespace Irstea\NgModelGeneratorBundle\Command;
 
 use ApiPlatform\Core\Documentation\Documentation;
+use Assert\Assertion;
 use Irstea\NgModelGeneratorBundle\ModelGenerator;
 use Irstea\NgModelGeneratorBundle\Writers\ConsoleWriter;
 use Irstea\NgModelGeneratorBundle\Writers\DirectoryWriter;
@@ -91,6 +92,9 @@ final class NgModelGenerateCommand extends Command
 
         if ($input->getOption('restrict')) {
             $globs = $input->getOption('restrict');
+            Assertion::isArray($globs);
+            Assertion::allString($globs);
+
             $writer = new FilteringFileWriter($writer, function (string $path) use ($globs): bool {
                 foreach ($globs as $glob) {
                     if (fnmatch($glob, $path, FNM_PATHNAME)) {
@@ -118,14 +122,17 @@ final class NgModelGenerateCommand extends Command
     private function openWriter(InputInterface $input, OutputInterface $output): MultiFileWriter
     {
         $zipPath = $input->getOption('zip');
+        Assertion::nullOrString($zipPath);
         if ($zipPath) {
-            $archive = $this->openZipArchive($zipPath, $input->getOption('force'));
+            $force = (bool) $input->getOption('force');
+            $archive = $this->openZipArchive($zipPath, $force);
             $output->write("Writing to ZIP archive: <info>$zipPath</info>\n");
 
             return new ZipWriter($archive);
         }
 
         $dirPath = $input->getOption('output');
+        Assertion::nullOrString($dirPath);
         if ($dirPath) {
             $output->write("Writing to directory: <info>$dirPath</info>\n");
 
diff --git a/src/php/Command/NgModelMetadataCommand.php b/src/php/Command/NgModelMetadataCommand.php
index 56dd8de87b09fa2e39c75b21eed77047759eafcb..2a2c72abda93158a36853d61499cb595e194ed31 100644
--- a/src/php/Command/NgModelMetadataCommand.php
+++ b/src/php/Command/NgModelMetadataCommand.php
@@ -22,8 +22,8 @@
 namespace Irstea\NgModelGeneratorBundle\Command;
 
 use ApiPlatform\Core\Documentation\Documentation;
+use Assert\Assertion;
 use Irstea\NgModelGeneratorBundle\Metadata\MetadataFactoryInterface;
-use Irstea\NgModelGeneratorBundle\ModelGenerator;
 use Irstea\NgModelGeneratorBundle\Models\PHPClass;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputArgument;
@@ -48,8 +48,8 @@ final class NgModelMetadataCommand extends Command
     /**
      * NgModelGenerateCommand constructor.
      *
-     * @param ModelGenerator $generator
-     * @param Documentation  $documentation
+     * @param MetadataFactoryInterface $metadataFactory
+     * @param Documentation            $documentation
      */
     public function __construct(
         MetadataFactoryInterface $metadataFactory,
@@ -78,6 +78,8 @@ final class NgModelMetadataCommand extends Command
     protected function execute(InputInterface $input, OutputInterface $output)
     {
         $classes = $input->getArgument('classes');
+        Assertion::isArray($classes);
+        Assertion::allString($classes);
 
         $metadata = [];
         foreach ($this->documentation->getResourceNameCollection() as $className) {
@@ -88,6 +90,8 @@ final class NgModelMetadataCommand extends Command
             $metadata[$class->getBaseName()] = $this->metadataFactory->getResourceMetadata($class);
         }
 
-        $output->writeln(\json_encode($metadata, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
+        $json = \json_encode($metadata, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
+        Assertion::string($json);
+        $output->writeln($json);
     }
 }
diff --git a/src/php/Iterators/CallbackFilterRecursorIterator.php b/src/php/Iterators/CallbackFilterRecursorIterator.php
index 8a7b28f824d5f7514d6eac8cc2c99c253aded252..e9eafe086710b59594d1e007acb70243cceb0a90 100644
--- a/src/php/Iterators/CallbackFilterRecursorIterator.php
+++ b/src/php/Iterators/CallbackFilterRecursorIterator.php
@@ -57,7 +57,7 @@ class CallbackFilterRecursorIterator extends RecursorIterator
      *
      * @return \RecursiveIterator
      */
-    protected function doGetChildren(\Iterator $iter): \RecursiveIterator
+    protected function doGetChildren(\Traversable $iter): \RecursiveIterator
     {
         return new self($iter, $this->filter);
     }
diff --git a/src/php/Iterators/IteratorBuilder.php b/src/php/Iterators/IteratorBuilder.php
index b808c1711cd5b485f8599b8776cd0f28aae38d12..72ed6a2223b29c1190f2741c1baad265194d5b7a 100644
--- a/src/php/Iterators/IteratorBuilder.php
+++ b/src/php/Iterators/IteratorBuilder.php
@@ -97,7 +97,7 @@ final class IteratorBuilder implements \IteratorAggregate
     }
 
     /**
-     * @param callable $param
+     * @param callable $where
      *
      * @return IteratorBuilder
      */
diff --git a/src/php/Iterators/RecursorIterator.php b/src/php/Iterators/RecursorIterator.php
index f1cab0827322ecb41413ecad79bf08d09f4e8cd9..7b8dea87733abffc9c42966e2ee64afcdd02dc0d 100644
--- a/src/php/Iterators/RecursorIterator.php
+++ b/src/php/Iterators/RecursorIterator.php
@@ -56,11 +56,11 @@ class RecursorIterator extends \IteratorIterator implements \RecursiveIterator
     }
 
     /**
-     * @param \Iterator $iter
+     * @param \Traversable $iter
      *
      * @return \RecursiveIterator
      */
-    protected function doGetChildren(\Iterator $iter): \RecursiveIterator
+    protected function doGetChildren(\Traversable $iter): \RecursiveIterator
     {
         return new self($iter);
     }
diff --git a/src/php/Iterators/UniqueFilter.php b/src/php/Iterators/UniqueFilter.php
index 29bd3987763b3151c68821ffc334d113d209a548..6092c19ea70b044e7fbf0e623191f87edc472b72 100644
--- a/src/php/Iterators/UniqueFilter.php
+++ b/src/php/Iterators/UniqueFilter.php
@@ -32,7 +32,7 @@ final class UniqueFilter
     private $seen = [];
 
     /**
-     * @param $value
+     * @param mixed $value
      *
      * @return bool
      */
diff --git a/src/php/Metadata/MetadataFactory.php b/src/php/Metadata/MetadataFactory.php
index 6878a345444040ade56ee3bba9f1394b8f307cdb..1c8ecab78c5c8055ee1541ba5620cdac21d50d9c 100644
--- a/src/php/Metadata/MetadataFactory.php
+++ b/src/php/Metadata/MetadataFactory.php
@@ -30,6 +30,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInte
 use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
 use ApiPlatform\Core\Metadata\Resource\ResourceMetadata as APIResourceMetadata;
 use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
+use Assert\Assertion;
 use Doctrine\Common\Inflector\Inflector;
 use Irstea\NgModelGeneratorBundle\Models\ClassName;
 use Irstea\NgModelGeneratorBundle\Models\PHPClass;
@@ -145,8 +146,7 @@ final class MetadataFactory implements MetadataFactoryInterface
         return new ResourceMetadata(
             $class,
             $parentClass ? PHPClass::get($parentClass->getName()) : null,
-            $metadata->getShortName(),
-            $metadata->getDescription(),
+            $metadata->getDescription() ?: '',
             $classMeta->isAbstract(),
             $defaultNormalization,
             $this->getOperations($class)
@@ -194,8 +194,11 @@ final class MetadataFactory implements MetadataFactoryInterface
             OperationType::ITEM       => $resourceMetadata->getItemOperations(),
             OperationType::COLLECTION => $resourceMetadata->getCollectionOperations(),
         ] as $type => $ops) {
+            if (!$ops) {
+                continue;
+            }
             foreach ($ops as $name => $operation) {
-                $operations[] = $this->getOperation($class, $resourceMetadata, $name, $type, $operation);
+                $operations[] = $this->getOperation($class, $resourceMetadata, (string) $name, $type, $operation);
             }
         }
 
@@ -224,22 +227,23 @@ final class MetadataFactory implements MetadataFactoryInterface
             $method = $this->operationMethodResolver->getCollectionOperationMethod($class->getFullName(), $name);
         }
 
+        $shortName = $resourceMetadata->getShortName();
+        Assertion::notNull($shortName);
         /** @noinspection PhpMethodParametersCountMismatchInspection */
-        $path = $this->operationPathResolver->resolveOperationPath($resourceMetadata->getShortName(), $operation, $type, $name);
+        $path = $this->operationPathResolver->resolveOperationPath($shortName, $operation, $type);
 
         // Suppression du suffixe de format, qui n'est pas utilisable en dehors de Symfony
         $path = \str_replace('.{_format}', '', $path);
 
         $getAttribute = function (string $attrName, $default) use ($resourceMetadata, $type, $name) {
             return $resourceMetadata->getTypedOperationAttribute($type, $name, $attrName, $default, true);
-
         };
 
         if ($type === OperationType::COLLECTION && $method === 'GET') {
             $filters = $this->getFilters($class, $getAttribute('filters', []));
 
-            $paginationEnabled = (bool)$getAttribute('pagination_enabled', true);
-            $paginationClientItemsPerPage = (bool)$getAttribute('pagination_client_items_per_page', true);
+            $paginationEnabled = (bool) $getAttribute('pagination_enabled', true);
+            $paginationClientItemsPerPage = (bool) $getAttribute('pagination_client_items_per_page', true);
 
             $pagination = $this->buildPagination($paginationEnabled, $paginationClientItemsPerPage);
         } else {
@@ -396,8 +400,9 @@ final class MetadataFactory implements MetadataFactoryInterface
                 $properties[$propertyName] = $property;
                 if ($property->isEmbedded()) {
                     $type = $property->getLeafType();
-                    if ($type->getClassName()) {
-                        $queue[] = PHPClass::get($type->getClassName());
+                    $className = $type->getClassName();
+                    if ($className) {
+                        $queue[] = PHPClass::get($className);
                     }
                 }
             }
diff --git a/src/php/Metadata/PropertyMetadata.php b/src/php/Metadata/PropertyMetadata.php
index 83b7b49f799aa98fe4e28adbb72a70ec10e2ace6..b9fc8640d565fc5308012bdae7405d23142bae13 100644
--- a/src/php/Metadata/PropertyMetadata.php
+++ b/src/php/Metadata/PropertyMetadata.php
@@ -21,6 +21,7 @@
 
 namespace Irstea\NgModelGeneratorBundle\Metadata;
 
+use Assert\Assertion;
 use Irstea\NgModelGeneratorBundle\Models\HasName;
 use Symfony\Component\PropertyInfo\Type;
 
@@ -66,6 +67,7 @@ class PropertyMetadata implements \JsonSerializable, HasName
      * @param string $description
      * @param Type   $type
      * @param bool   $identifier
+     * @param bool   $nullable
      * @param bool   $readable
      * @param bool   $writable
      * @param bool   $initializable
@@ -132,8 +134,9 @@ class PropertyMetadata implements \JsonSerializable, HasName
     public function getLeafType(): Type
     {
         $type = $this->type;
-        while ($type && $type->isCollection() && $type->getCollectionValueType()) {
+        while ($type->isCollection() && $type->getCollectionValueType()) {
             $type = $type->getCollectionValueType();
+            Assertion::notNull($type);
         }
 
         return $type;
diff --git a/src/php/Metadata/PropertyMetadataFactory.php b/src/php/Metadata/PropertyMetadataFactory.php
index 55d0e2542ac97289c3f118901575397214f3b4a7..a184aef168a0a8313417e45ce84a1bf7ed7d72f8 100644
--- a/src/php/Metadata/PropertyMetadataFactory.php
+++ b/src/php/Metadata/PropertyMetadataFactory.php
@@ -112,8 +112,8 @@ class PropertyMetadataFactory
             $typeMeta,
             $propertyMeta->isIdentifier() ?: false,
             $nullable,
-            $propertyMeta->isReadable(),
-            $propertyMeta->isWritable(),
+            $propertyMeta->isReadable() ?: false,
+            $propertyMeta->isWritable() ?: false,
             (bool) $propertyMeta->isInitializable(),
             $link,
             $embedded
@@ -121,7 +121,7 @@ class PropertyMetadataFactory
     }
 
     /**
-     * @param $propertyMeta
+     * @param APIPropertyMetadata $propertyMeta
      *
      * @return array
      */
@@ -211,9 +211,8 @@ class PropertyMetadataFactory
     }
 
     /**
-     * @param string              $mode
+     * @param bool                $isResource
      * @param APIPropertyMetadata $propertyMeta
-     * @param mixed               $isResource
      *
      * @return bool
      */
@@ -228,10 +227,10 @@ class PropertyMetadataFactory
                 return $propertyMeta->isWritable() || $propertyMeta->isInitializable();
 
             case self::MODE_READ:
-                return $propertyMeta->isReadable();
+                return $propertyMeta->isReadable() ?: false;
 
             case self::MODE_UPDATE:
-                return $propertyMeta->isWritable();
+                return $propertyMeta->isWritable() ?: false;
 
             default:
                 return $propertyMeta->isReadable() || $propertyMeta->isWritable();
diff --git a/src/php/Metadata/ResourceClassHierarchy.php b/src/php/Metadata/ResourceClassHierarchy.php
index 6d6be8bddbb2424885fc5d101d893adbffb8b8f8..fc4500a1035b724ca065a67f02b2c64caeea9318 100644
--- a/src/php/Metadata/ResourceClassHierarchy.php
+++ b/src/php/Metadata/ResourceClassHierarchy.php
@@ -59,7 +59,7 @@ final class ResourceClassHierarchy implements ClassHierarchy
         $reflClass = new \ReflectionClass($className);
         $reflParent = $reflClass->getParentClass();
         if (!$reflParent) {
-            $this->parents[$className] = null;
+            unset($this->parents[$className]);
 
             return;
         }
diff --git a/src/php/Metadata/ResourceMetadata.php b/src/php/Metadata/ResourceMetadata.php
index 37e110561772c864fcbce05bdfbca6049b1eff91..8a7345c91a6b220dc5b6b1578a437a342ee9ed9a 100644
--- a/src/php/Metadata/ResourceMetadata.php
+++ b/src/php/Metadata/ResourceMetadata.php
@@ -51,7 +51,6 @@ class ResourceMetadata implements ClassName
      *
      * @param ClassName             $class
      * @param ClassName|null        $parentClass
-     * @param string                $shortName
      * @param string                $description
      * @param bool                  $abstract
      * @param SerializationMetadata $defaultNormalization
@@ -60,7 +59,6 @@ class ResourceMetadata implements ClassName
     public function __construct(
         ClassName $class,
         ?ClassName $parentClass,
-        string $shortName,
         string $description,
         bool $abstract,
         SerializationMetadata $defaultNormalization,
diff --git a/src/php/ModelGenerator.php b/src/php/ModelGenerator.php
index 971e7d6e6288c340e84267a2fa9cb6596d3fdd53..c2543d9e73cda809da616040a693dbe95b7a0e6e 100644
--- a/src/php/ModelGenerator.php
+++ b/src/php/ModelGenerator.php
@@ -40,7 +40,6 @@ use Irstea\NgModelGeneratorBundle\Models\Types\StringConst;
 use Irstea\NgModelGeneratorBundle\Models\Types\Type;
 use Irstea\NgModelGeneratorBundle\Models\Types\Union;
 use Irstea\NgModelGeneratorBundle\Writers\MultiFileWriter;
-use Irstea\NgModelGeneratorBundle\Writers\Writer;
 use Symfony\Component\PropertyInfo\Type as PHPType;
 use Twig\Environment;
 
@@ -58,7 +57,7 @@ final class ModelGenerator
     /** @var Documentation */
     private $documentation;
 
-    /** @var TypeFactory */
+    /** @var TypeFactoryInterface */
     private $typeFactory;
 
     /** @var SerializationMapperFactoryInterface */
@@ -82,8 +81,12 @@ final class ModelGenerator
      * Génère les modèles Typescript.
      * Cette méthode n'est pas réentrante.
      *
-     * @param Documentation $doc
-     * @param Writer        $writer
+     * @param Documentation   $doc
+     * @param MultiFileWriter $writer
+     *
+     * @throws \Twig_Error_Loader
+     * @throws \Twig_Error_Runtime
+     * @throws \Twig_Error_Syntax
      */
     public function generate(Documentation $doc, MultiFileWriter $writer): void
     {
@@ -108,8 +111,6 @@ final class ModelGenerator
      * @throws \Twig_Error_Loader
      * @throws \Twig_Error_Runtime
      * @throws \Twig_Error_Syntax
-     *
-     * @return string
      */
     private function doGenerate(MultiFileWriter $writer): void
     {
@@ -170,8 +171,6 @@ final class ModelGenerator
      * @throws \Twig_Error_Loader
      * @throws \Twig_Error_Runtime
      * @throws \Twig_Error_Syntax
-     *
-     * @return string
      */
     private function generateFile(MultiFileWriter $writer, string $path, array $context): void
     {
diff --git a/src/php/Models/ClassInfo.php b/src/php/Models/ClassInfo.php
index cb303e494104c0dc6717b268b758489f9266b10c..b6c93e869d35a76b0229b8070edba76c48f31326 100644
--- a/src/php/Models/ClassInfo.php
+++ b/src/php/Models/ClassInfo.php
@@ -22,6 +22,7 @@
 namespace Irstea\NgModelGeneratorBundle\Models;
 
 use Irstea\NgModelGeneratorBundle\Exceptions\DomainException;
+use Irstea\NgModelGeneratorBundle\Metadata\PropertyMetadata;
 
 /**
  * Class ClassInfo.
@@ -39,10 +40,10 @@ final class ClassInfo implements ClassName
     /** @var self|false|null */
     private $parent = false;
 
-    /** @var HasName[] */
+    /** @var PropertyMetadata[] */
     private $virtualProperties = [];
 
-    /** @var HasName[] */
+    /** @var PropertyMetadata[] */
     private $concreteProperties = [];
 
     /** @var self[] */
@@ -57,11 +58,11 @@ final class ClassInfo implements ClassName
     /**
      * ClassInfo constructor.
      *
-     * @param ClassName $class
-     * @param HasName[] $properties
-     * @param bool      $abstract
+     * @param ClassName          $class
+     * @param PropertyMetadata[] $properties
+     * @param bool               $abstract
      */
-    public function __construct(ClassName $class, array $properties = [], bool $abstract)
+    public function __construct(ClassName $class, array $properties = [], bool $abstract = false)
     {
         $this->class = $class;
         $this->abstract = $abstract;
@@ -99,7 +100,7 @@ final class ClassInfo implements ClassName
     /**
      * Get properties.
      *
-     * @return HasName[]
+     * @return PropertyMetadata[]
      */
     public function getVirtualProperties(): array
     {
@@ -109,7 +110,7 @@ final class ClassInfo implements ClassName
     /**
      * Get properties.
      *
-     * @return HasName[]
+     * @return PropertyMetadata[]
      */
     public function getConcreteProperties(): array
     {
diff --git a/src/php/Models/Types/AbstractType.php b/src/php/Models/Types/AbstractType.php
index 018cb9090aa5e2c2acaeb67c4e40713bbe7c85ae..a9976ed960f6abc18c9c41722f4af6809ca8387a 100644
--- a/src/php/Models/Types/AbstractType.php
+++ b/src/php/Models/Types/AbstractType.php
@@ -62,7 +62,7 @@ abstract class AbstractType implements Type
     }
 
     /**
-     * @return string
+     * @return mixed
      */
     public function jsonSerialize()
     {
diff --git a/src/php/Models/Types/Objects/AnonymousObject.php b/src/php/Models/Types/Objects/AnonymousObject.php
index ffd2b6c8e0bee8871c3f54311d9e91782a1a3ebc..9799b69f2c22f67fcd086f1e6dbe45459de32d2a 100644
--- a/src/php/Models/Types/Objects/AnonymousObject.php
+++ b/src/php/Models/Types/Objects/AnonymousObject.php
@@ -167,8 +167,6 @@ class AnonymousObject extends AbstractType
     }
 
     /**
-     * @param bool $multiline
-     *
      * @return array
      */
     protected function getMethodDeclarations(): array
diff --git a/src/php/Models/Types/Objects/Repository.php b/src/php/Models/Types/Objects/Repository.php
index cc6e36a88f392b228c4d72deed4e27942cb9aec3..e6a1165b74dd6cb5037dea25b6106b38b899071c 100644
--- a/src/php/Models/Types/Objects/Repository.php
+++ b/src/php/Models/Types/Objects/Repository.php
@@ -122,24 +122,6 @@ final class Repository extends ClassType
         return $this->identifier;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function getMethodDeclarations(): array
-    {
-        $parts = $this->getStaticMethodDeclarations();
-
-        $parts[] = $this->getConstructor();
-
-        $parts[] = $this->getResolveImplementation();
-
-        foreach ($this->getOperations() as $operation) {
-            $parts[] = $operation->getDeclaration() . "\n";
-        }
-
-        return $parts;
-    }
-
     /**
      * {@inheritdoc}
      */
@@ -195,77 +177,6 @@ CODE
         );
     }
 
-    /**
-     * @return array
-     */
-    private function getStaticMethodDeclarations(): array
-    {
-        if (!$this->iri) {
-            return [];
-        }
-
-        $parts = [];
-
-        // Vérification d'IRI
-        $parts[] = sprintf(
-        /* @lang typescript */
-            <<<'CODE'
-/**
- * Regexp pour reconnaître une IRI de cette ressource.
- */
-public static readonly IRI_PATTERN = /^%s$/ui;
-
-/**
- * Vérifie si le paramètre est une IRI valide de ce repository.
- */                          
-public static isIRI(data: string): data is IRI<%s> {
-  return %s.IRI_PATTERN.test(data);
-}
-CODE
-            ,
-            $this->iri->getPattern(),
-            $this->resourceType->getUsage(),
-            $this->getName()
-        );
-
-        $idProp = $this->identifier;
-        $idParam = $this->iri->getIdentifier();
-
-        if ($idProp && $idParam) {
-            $idType = $idProp->getType();
-
-            $parts[] = sprintf(
-            /* @lang typescript */
-                <<<'CODE'
-
-/**
- * Extrait l'identifiant d'une IRI ou vérifie un identifiant.
- */
-public static getID(data: %s): %s {
-  const match = %s.IRI_PATTERN.exec(data);
-  if (match !== null) {
-    return %s;
-  }
-  if (%s) {
-    return %s;
-  }
-  throw new Error(`Cannot extract %s id from ${data}`);
-}
-CODE
-                ,
-                $idParam->getType()->getUsage(),
-                $idType->getUsage(),
-                $this->getName(),
-                $idType->castToStringOrStringArray('match[1]'),
-                $idType->checkType('data'),
-                $idType->castToStringOrStringArray('data'),
-                $this->getResourceName()
-            );
-        }
-
-        return $parts;
-    }
-
     /**
      * {@inheritdoc}
      */
diff --git a/src/php/Models/Types/Operations/Parameter.php b/src/php/Models/Types/Operations/Parameter.php
index fdd2c2b181f4f6f7775369f492883aac3889aaf3..a69b263ce4b58e9bb1c32f470595007a61595a27 100644
--- a/src/php/Models/Types/Operations/Parameter.php
+++ b/src/php/Models/Types/Operations/Parameter.php
@@ -42,7 +42,7 @@ class Parameter implements \IteratorAggregate, \JsonSerializable
     private $optional = false;
 
     /**
-     * @var string
+     * @var string|null
      */
     private $default;
 
diff --git a/src/php/Models/Types/Reference.php b/src/php/Models/Types/Reference.php
index 78ed019f93f16476bb48ab71ec3993f09ba5d9b8..61f41add399ada1893e63e6df8814410ace0f307 100644
--- a/src/php/Models/Types/Reference.php
+++ b/src/php/Models/Types/Reference.php
@@ -36,7 +36,7 @@ class Reference extends AbstractType implements Deferred
     /** @var string */
     private $name;
 
-    /** @var bool */
+    /** @var int */
     private $state = self::UNRESOLVED;
 
     /**
diff --git a/src/php/Models/Types/Union.php b/src/php/Models/Types/Union.php
index c488e9f57730665e1d02db68dde1aedac5343c57..52b1ea97c32ad71095129289b7fb8cb2e4a1a529 100644
--- a/src/php/Models/Types/Union.php
+++ b/src/php/Models/Types/Union.php
@@ -112,7 +112,7 @@ final class Union extends AbstractType
             case 0:
                 return BuiltinType::get('never');
             case 1:
-                return array_shift($types);
+                return $types[0];
             default:
                 return new Union($types);
         }
diff --git a/src/php/OperationMapper.php b/src/php/OperationMapper.php
index 247a7921cb414d755ab20ad035dee9eee52a8b71..20f5c91068239322b2d9f5f4ccc76810b02a6222 100644
--- a/src/php/OperationMapper.php
+++ b/src/php/OperationMapper.php
@@ -21,6 +21,7 @@
 
 namespace Irstea\NgModelGeneratorBundle;
 
+use Assert\Assertion;
 use Doctrine\Common\Inflector\Inflector;
 use Irstea\NgModelGeneratorBundle\Exceptions\InvalidArgumentException;
 use Irstea\NgModelGeneratorBundle\Metadata\OperationMetadata;
@@ -106,7 +107,7 @@ final class OperationMapper
 
         $callPath = $path = $this->pathParser->parse($this->operation->getPath(), $this->operation->getRequirements());
 
-        $iriParameterName = null;
+        $iriParameter = $iriParameterName = null;
 
         if ($this->iri) {
             $iriParameter = new Parameter('iri', Placeholder::get(sprintf('IRI<%s>', $this->operation->getClassName())));
@@ -115,6 +116,7 @@ final class OperationMapper
         }
 
         if ($callPath === $this->iri) {
+            Assertion::notNull($iriParameter);
             $opParameters[] = $iriParameter;
             $iriParameterName = $iriParameter->getName();
             $callParameters[] = $iriParameterName . ' as any';
@@ -257,7 +259,9 @@ final class OperationMapper
                     }
                 }
 
-                $propName = Inflector::camelize(preg_replace('/\W+/', '_', $name));
+                /** @var string $alphanumName */
+                $alphanumName = preg_replace('/\W+/', '_', $name);
+                $propName = Inflector::camelize($alphanumName);
 
                 $type = $this->resolveFilterType($filterType, $type);
 
diff --git a/src/php/PathParser.php b/src/php/PathParser.php
index 653daa220df992449e0052a3d18aad34e3d8bfd1..1b53b70d09a22f5f209cf1ee5a059bd452eab84a 100644
--- a/src/php/PathParser.php
+++ b/src/php/PathParser.php
@@ -21,6 +21,7 @@
 
 namespace Irstea\NgModelGeneratorBundle;
 
+use Assert\Assertion;
 use Irstea\NgModelGeneratorBundle\Models\Types\BuiltinType;
 use Irstea\NgModelGeneratorBundle\Models\Types\Objects\Property;
 use Irstea\NgModelGeneratorBundle\Models\Types\Operations\FixedPathPart;
@@ -52,6 +53,7 @@ final class PathParser implements PathParserInterface
     public function parse(string $path, array $requirements): Path
     {
         $stringParts = preg_split('/\{(\w+)\}/', $path, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+        Assertion::isArray($stringParts);
 
         $parts = [];
 
diff --git a/src/php/SerializationMapper.php b/src/php/SerializationMapper.php
index bce64a1d0494a4bcbea437ae00de5757166c2ad2..7ec928aecf34fccf3ad6aa25f9e951f7e1a15053 100644
--- a/src/php/SerializationMapper.php
+++ b/src/php/SerializationMapper.php
@@ -21,6 +21,7 @@
 
 namespace Irstea\NgModelGeneratorBundle;
 
+use Assert\Assertion;
 use Irstea\NgModelGeneratorBundle\Exceptions\DomainException;
 use Irstea\NgModelGeneratorBundle\Metadata\PropertyMetadata;
 use Irstea\NgModelGeneratorBundle\Metadata\RepresentationMetadata;
@@ -164,6 +165,7 @@ final class SerializationMapper implements TypeFactoryInterface
         $desc = trim(implode("\n", $desc));
 
         if ($classInfo->isUnion()) {
+            /** @var Type[] $types */
             $types = [];
             foreach ($classInfo->iterateInterfaceDescendants() as $child) {
                 $types[] = $this->get($child->getFullName());
@@ -173,7 +175,10 @@ final class SerializationMapper implements TypeFactoryInterface
                 case 0:
                     throw new DomainException(sprintf('Union with no children: %s', $repr));
                 case 1:
-                    return new Reference($types[0]);
+                    $ref = new Reference($types[0]->getUsage());
+                    $ref->resolve($types[0]);
+
+                    return $ref;
                 default:
                     return new Alias($repr->getName(), Union::create($types), $desc);
             }
@@ -294,44 +299,53 @@ final class SerializationMapper implements TypeFactoryInterface
      */
     private function mapProperty(PropertyMetadata $propertyMeta): Property
     {
-        $typeMeta = $propertyMeta->getType();
-        \assert($typeMeta !== null);
-
-        $collections = [];
-        $leafType = $typeMeta;
-        while ($leafType->isCollection()) {
-            $collections[] = $leafType->getCollectionKeyType();
-            $leafType = $leafType->getCollectionValueType();
-        }
-
-        $leafClassName = $leafType->getClassName();
-        if ($propertyMeta->isLink() && !$propertyMeta->isEmbedded()) {
-            $type = $this->createIRI([PHPClass::get($leafClassName)]);
-        } else {
-            $type = $this->get($leafClassName ?: $leafType->getBuiltinType());
-        }
-
-        $number = BuiltinType::get('number');
-        foreach (\array_reverse($collections) as $indexTypeMeta) {
-            /** @var APIType $indexTypeMeta */
-            $indexType = $this->get($indexTypeMeta->getClassName() ?: $indexTypeMeta->getBuiltinType());
-            if ($indexType === $number) {
-                $type = new ArrayType($type);
-            } else {
-                throw new DomainException("Cannot create collection with key $indexType");
-            }
-        }
-
         return new Property(
             $propertyMeta->getName(),
             $propertyMeta->getDescription() ?: '',
-            $type,
+            $this->mapType($propertyMeta->getType(), $propertyMeta->isLink() && !$propertyMeta->isEmbedded()),
             $propertyMeta->isIdentifier(),
             $propertyMeta->isNullable(),
             !$propertyMeta->isWritable()
         );
     }
 
+    /**
+     * @param APIType $type
+     * @param bool    $isLink
+     *
+     * @return Type
+     */
+    private function mapType(?APIType $type, bool $isLink = false): Type
+    {
+        if ($type === null) {
+            return BuiltinType::get('any');
+        }
+
+        if (!$type->isCollection()) {
+            $className = $type->getClassName();
+            if ($className === null) {
+                return $this->get($type->getBuiltinType());
+            }
+            if ($isLink) {
+                return $this->createIRI([PHPClass::get($className)]);
+            }
+
+            return $this->get($className);
+        }
+
+        $indexType = $type->getCollectionKeyType();
+        Assertion::notNull($indexType, 'Cannot handle collection with undefined index type');
+
+        $builtinType = $indexType->getBuiltinType();
+        if ($builtinType !== 'int') {
+            throw new \InvalidArgumentException("Cannot handle collection with index type $builtinType");
+        }
+
+        $valueType = $this->mapType($type->getCollectionValueType(), $isLink);
+
+        return new ArrayType($valueType);
+    }
+
     /**
      * @param ClassName $class
      *
@@ -343,7 +357,11 @@ final class SerializationMapper implements TypeFactoryInterface
             $this->init();
         }
 
-        return $this->classInfo[$class->getFullName()];
+        $name = $class->getFullName();
+        $info = $this->classInfo[$name] ?? null;
+        Assertion::notNull($info, 'Unknown class $name');
+
+        return $info;
     }
 
     private function init(): void
@@ -363,8 +381,9 @@ final class SerializationMapper implements TypeFactoryInterface
         // Crée les liens de parenté
         foreach ($reprs as $className => $repr) {
             $classInfo = $this->classInfo[$className];
-            if ($repr->getParent()) {
-                $classInfo->setParent($this->classInfo[$repr->getParent()->getFullName()]);
+            $parent = $repr->getParent();
+            if ($parent) {
+                $classInfo->setParent($this->classInfo[$parent->getFullName()]);
             }
         }
 
@@ -372,6 +391,5 @@ final class SerializationMapper implements TypeFactoryInterface
         foreach ($this->classInfo as $className => $classInfo) {
             $classInfo->rearrangeHiearchy();
         }
-        //echo json_encode($this->classInfo, \JSON_PRETTY_PRINT) . "\n";
     }
 }
diff --git a/src/php/TypeFactoryInterface.php b/src/php/TypeFactoryInterface.php
index bfd16413b05bbeee397c32936ec8f7d54ac80b06..4b2ae3f9e80eb61b8c3f3260c76ba3afd57dbeb1 100644
--- a/src/php/TypeFactoryInterface.php
+++ b/src/php/TypeFactoryInterface.php
@@ -46,7 +46,7 @@ interface TypeFactoryInterface
     /**
      * @param string $name
      *
-     * @return Type
+     * @return Deferred
      */
     public function defer(string $name): Deferred;
 
diff --git a/src/php/Writers/DirectoryWriter.php b/src/php/Writers/DirectoryWriter.php
index e11a674108cc4013cb9cf4b0a74bd2152688f7e8..4eca46ad18a6521c037bcfcc5a5ba0d9a44a9c80 100644
--- a/src/php/Writers/DirectoryWriter.php
+++ b/src/php/Writers/DirectoryWriter.php
@@ -21,6 +21,8 @@
 
 namespace Irstea\NgModelGeneratorBundle\Writers;
 
+use Assert\Assertion;
+
 /**
  * Class DirectoryWriter.
  */
@@ -52,7 +54,10 @@ final class DirectoryWriter implements MultiFileWriter
             throw new \RuntimeException(sprintf('Directory "%s" was not created', $fileDir));
         }
 
-        return new StreamWriter(fopen($filePath, 'wb'));
+        $fh = fopen($filePath, 'wb');
+        Assertion::isResource($fh);
+
+        return new StreamWriter($fh);
     }
 
     public function close(): void
diff --git a/src/php/Writers/ZipWriter.php b/src/php/Writers/ZipWriter.php
index 9f3e917f59ac62864b75826b8881f4f491f054fc..b60380456847d8773fb3bd09d1937261a0e7581b 100644
--- a/src/php/Writers/ZipWriter.php
+++ b/src/php/Writers/ZipWriter.php
@@ -32,7 +32,7 @@ class ZipWriter implements MultiFileWriter
     /**
      * ZipWriter constructor.
      *
-     * @param $archive
+     * @param \ZipArchive $archive
      */
     public function __construct(\ZipArchive $archive)
     {