Repository: JsonMapper/JsonMapper (php)

Issue:
Support mapping interfaces and abstract classes
In its current state JsonMapper requires exact and concrete matches when trying to map object. E.g. mapping to an abstract class or interface is currently not possible. Secondly having an API that responds with two or more different objects (e.g. a `Square` or a `Circle` which both are a `Shape`) cannot be properly mapped.

In order to support interfaces and abstract classes the mapper should be able to handle those while trying to map the JSON. Specific input from the programmer is needed to match an interface or abstract class to a concrete class. This can be achieved similar to how a Dependency Injection container works. 
Currently JsonMapper already supports factories for custom object creation this might be a good way in in order to achieve this feature.

Alternatively this feature could be discarded but having this would be a nice addition to the library and become beneficial for future projects which embrace the type safe approach the PHP language is currently taking.

An example of a failing test (fails with `Error : Cannot instantiate abstract class JsonMapper\Tests\Integration\Shape`) is:
```php
<?php

declare(strict_types=1);

namespace JsonMapper\Tests\Integration;

use JsonMapper\JsonMapperFactory;
use PHPUnit\Framework\TestCase;

class InheritanceTest extends TestCase
{
    public function testCanMapToAnAbstractClass(): void
    {
        $mapper = (new JsonMapperFactory())->bestFit();
        $response = new Response();
        $mapper->mapObjectFromString(
            '{"shape": {"length": 4, "width": 4}}',
            $response
        );

        self::assertEquals(16, $response->shape->getCircumference());

    }
}

class Response {
    /** @var Shape */
    public $shape;
}

abstract class Shape {
    abstract public function getCircumference(): float;

    public function getType(): string
    {
        return get_class();
    }
}

class Square extends Shape {
    /** @var int */
    private $width;
    /** @var int */
    private $length;

    public function __construct(int $width, int $length)
    {
        $this->width = $width;
        $this->length = $length;
    }


    public function getCircumference(): float
    {
        return ($this->length * 2) + ($this->width * 2);
    }
}
```

Pull request:
Add intial support for mapping interfaces and abstract classes
Would resolve #77 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a427122..cab0bee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 ### Added
 - Map to stdClass was added to allow for generic objects [PR#89](https://github.com/JsonMapper/JsonMapper/pull/89)
+- Map interfaces and abstract classes using factories [PR#84](https://github.com/JsonMapper/JsonMapper/pull/84)
 
 ## [2.4.1] - 2021-05-07
 ### Fixed
diff --git a/src/Handler/ClassFactoryRegistry.php b/src/Handler/FactoryRegistry.php
similarity index 65%
rename from src/Handler/ClassFactoryRegistry.php
rename to src/Handler/FactoryRegistry.php
index e66bcad..9e63f7d 100644
--- a/src/Handler/ClassFactoryRegistry.php
+++ b/src/Handler/FactoryRegistry.php
@@ -6,24 +6,25 @@ namespace JsonMapper\Handler;
 
 use JsonMapper\Exception\ClassFactoryException;
 
-class ClassFactoryRegistry
+class FactoryRegistry
 {
     /** @var callable[] */
     private $factories = [];
 
-    public function loadNativePhpClassFactories(): self
+    public static function WithNativePhpClassesAdded(): self
     {
-        $this->addFactory(\DateTime::class, static function (string $value) {
+        $factory = new self();
+        $factory->addFactory(\DateTime::class, static function (string $value) {
             return new \DateTime($value);
         });
-        $this->addFactory(\DateTimeImmutable::class, static function (string $value) {
+        $factory->addFactory(\DateTimeImmutable::class, static function (string $value) {
             return new \DateTimeImmutable($value);
         });
-        $this->addFactory(\stdClass::class, static function ($value) {
+        $factory->addFactory(\stdClass::class, static function ($value) {
             return (object) $value;
         });
 
-        return $this;
+        return $factory;
     }
 
     public function addFactory(string $className, callable $factory): self
@@ -32,14 +33,14 @@ class ClassFactoryRegistry
             throw ClassFactoryException::forDuplicateClassname($className);
         }
 
-        $this->factories[$this->sanatizeClassName($className)] = $factory;
+        $this->factories[$this->sanitiseClassName($className)] = $factory;
 
         return $this;
     }
 
     public function hasFactory(string $className): bool
     {
-        return array_key_exists($this->sanatizeClassName($className), $this->factories);
+        return array_key_exists($this->sanitiseClassName($className), $this->factories);
     }
 
     /**
@@ -52,12 +53,12 @@ class ClassFactoryRegistry
             throw ClassFactoryException::forMissingClassname($className);
         }
 
-        $factory = $this->factories[$this->sanatizeClassName($className)];
+        $factory = $this->factories[$this->sanitiseClassName($className)];
 
         return $factory($params);
     }
 
-    private function sanatizeClassName(string $className): string
+    private function sanitiseClassName(string $className): string
     {
         /* Erase leading slash as ::class doesnt contain leading slash */
         if (strpos($className, '\\') === 0) {
diff --git a/src/Handler/PropertyMapper.php b/src/Handler/PropertyMapper.php
index 8a3eb7b..5e5e467 100644
--- a/src/Handler/PropertyMapper.php
+++ b/src/Handler/PropertyMapper.php
@@ -6,6 +6,7 @@ namespace JsonMapper\Handler;
 
 use JsonMapper\Enums\ScalarType;
 use JsonMapper\Enums\Visibility;
+use JsonMapper\Exception\ClassFactoryException;
 use JsonMapper\JsonMapperInterface;
 use JsonMapper\ValueObjects\Property;
 use JsonMapper\ValueObjects\PropertyMap;
@@ -14,17 +15,25 @@ use JsonMapper\Wrapper\ObjectWrapper;
 
 class PropertyMapper
 {
-    /** @var ClassFactoryRegistry */
+    /** @var FactoryRegistry */
     private $classFactoryRegistry;
+    /**@var FactoryRegistry */
+    private $nonInstantiableTypeResolver;
 
-    public function __construct(ClassFactoryRegistry $classFactoryRegistry = null)
-    {
+    public function __construct(
+        FactoryRegistry $classFactoryRegistry = null,
+        FactoryRegistry $nonInstantiableTypeResolver = null
+    ) {
         if ($classFactoryRegistry === null) {
-            $classFactoryRegistry = new ClassFactoryRegistry();
-            $classFactoryRegistry->loadNativePhpClassFactories();
+            $classFactoryRegistry = FactoryRegistry::WithNativePhpClassesAdded();
+        }
+
+        if ($nonInstantiableTypeResolver === null) {
+            $nonInstantiableTypeResolver = new FactoryRegistry();
         }
 
         $this->classFactoryRegistry = $classFactoryRegistry;
+        $this->nonInstantiableTypeResolver = $nonInstantiableTypeResolver;
     }
 
     public function __invoke(
@@ -142,10 +151,7 @@ class PropertyMapper
 
                 // Single existing class @todo how do you know it was the correct type?
                 if (class_exists($type->getType())) {
-                    $className = $type->getType();
-                    $instance = new $className();
-                    $mapper->mapObject($value, $instance);
-                    return $instance;
+                    return $this->mapToObject($type->getType(), $value, false, $mapper);
                 }
             }
         }
@@ -217,17 +223,34 @@ class PropertyMapper
     {
         if ($asArray) {
             return array_map(
-                static function ($v) use ($type, $mapper): object {
-                    $instance = new $type();
-                    $mapper->mapObject($v, $instance);
-                    return $instance;
+                function ($v) use ($type, $mapper): object {
+                    return $this->mapToObject($type, $v, false, $mapper);
                 },
                 (array) $value
             );
         }
 
+        $reflectionType = new \ReflectionClass($type);
+        if (!$reflectionType->isInstantiable()) {
+            return $this->resolveUnInstantiableType($type, $value, $mapper);
+        }
+
         $instance = new $type();
         $mapper->mapObject($value, $instance);
         return $instance;
     }
+
+    /**
+     * @param mixed $value
+     */
+    private function resolveUnInstantiableType(string $type, $value, JsonMapperInterface $mapper): object
+    {
+        try {
+            $instance = $this->nonInstantiableTypeResolver->create($type, $value);
+            $mapper->mapObject($value, $instance);
+            return $instance;
+        } catch (ClassFactoryException $e) {
+            throw new \RuntimeException("Unable to resolve un-instantiable {$type} as no factory was registered", 0, $e);
+        }
+    }
 }
diff --git a/src/Helpers/UseStatementHelper.php b/src/Helpers/UseStatementHelper.php
index 4470a2c..db9eee3 100644
--- a/src/Helpers/UseStatementHelper.php
+++ b/src/Helpers/UseStatementHelper.php
@@ -36,7 +36,7 @@ class UseStatementHelper
 
         try {
             $ast = $parser->parse($contents);
-        } catch (\Throwable $e ) {
+        } catch (\Throwable $e) {
             throw new \RuntimeException("Something went wrong when parsing {$class->getFileName()}", 0, $e);
         }
 


Test patch:
diff --git a/tests/Implementation/Models/AbstractShape.php b/tests/Implementation/Models/AbstractShape.php
new file mode 100644
index 0000000..dc9cca2
--- /dev/null
+++ b/tests/Implementation/Models/AbstractShape.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Models;
+
+abstract class AbstractShape implements IShape
+{
+    abstract public function getCircumference(): float;
+
+    public function getType(): string
+    {
+        return __CLASS__;
+    }
+}
diff --git a/tests/Implementation/Models/Circle.php b/tests/Implementation/Models/Circle.php
new file mode 100644
index 0000000..416bdef
--- /dev/null
+++ b/tests/Implementation/Models/Circle.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Models;
+
+class Circle extends AbstractShape
+{
+    public $radius;
+
+    public function getCircumference(): float
+    {
+        return 2 * $this->radius * M_PI;
+    }
+}
diff --git a/tests/Implementation/Models/IShape.php b/tests/Implementation/Models/IShape.php
new file mode 100644
index 0000000..dc81e8f
--- /dev/null
+++ b/tests/Implementation/Models/IShape.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Models;
+
+interface IShape
+{
+    public function getType(): string;
+    public function getCircumference(): float;
+}
diff --git a/tests/Implementation/Models/ShapeInstanceFactory.php b/tests/Implementation/Models/ShapeInstanceFactory.php
new file mode 100644
index 0000000..89b4b8b
--- /dev/null
+++ b/tests/Implementation/Models/ShapeInstanceFactory.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Models;
+
+class ShapeInstanceFactory
+{
+    public function __invoke(\stdClass $data): IShape
+    {
+        switch ($data->type) {
+            case 'square':
+                return new Square();
+            case 'circle':
+                return new Circle();
+            default:
+                throw new \RuntimeException("Unable to create shape for type {$data->type}");
+        }
+    }
+}
diff --git a/tests/Implementation/Models/Square.php b/tests/Implementation/Models/Square.php
new file mode 100644
index 0000000..7b67114
--- /dev/null
+++ b/tests/Implementation/Models/Square.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Models;
+
+class Square extends AbstractShape
+{
+    /** @var int */
+    public $width;
+    /** @var int */
+    public $length;
+
+    public function __construct(int $width = null, int $length = null)
+    {
+        $this->width = $width;
+        $this->length = $length;
+    }
+
+    public function getCircumference(): float
+    {
+        return ($this->length * 2) + ($this->width * 2);
+    }
+}
diff --git a/tests/Implementation/Models/Wrappers/AbstractShapeWrapper.php b/tests/Implementation/Models/Wrappers/AbstractShapeWrapper.php
new file mode 100644
index 0000000..54f8971
--- /dev/null
+++ b/tests/Implementation/Models/Wrappers/AbstractShapeWrapper.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Models\Wrappers;
+
+use JsonMapper\Tests\Implementation\Models\AbstractShape;
+use JsonMapper\Tests\Implementation\Models\IShape;
+
+class AbstractShapeWrapper implements IShapeAware
+{
+    /** @var AbstractShape */
+    public $shape;
+
+    public function getShape(): IShape
+    {
+        return $this->shape;
+    }
+}
diff --git a/tests/Implementation/Models/Wrappers/IShapeAware.php b/tests/Implementation/Models/Wrappers/IShapeAware.php
new file mode 100644
index 0000000..f597bea
--- /dev/null
+++ b/tests/Implementation/Models/Wrappers/IShapeAware.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Models\Wrappers;
+
+use JsonMapper\Tests\Implementation\Models\IShape;
+
+interface IShapeAware
+{
+    public function getShape(): IShape;
+}
diff --git a/tests/Implementation/Models/Wrappers/IShapeWrapper.php b/tests/Implementation/Models/Wrappers/IShapeWrapper.php
new file mode 100644
index 0000000..2fc6d22
--- /dev/null
+++ b/tests/Implementation/Models/Wrappers/IShapeWrapper.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Implementation\Models\Wrappers;
+
+use JsonMapper\Tests\Implementation\Models\IShape;
+
+class IShapeWrapper implements IShapeAware
+{
+    /** @var IShape */
+    public $shape;
+
+    public function getShape(): IShape
+    {
+        return $this->shape;
+    }
+}
diff --git a/tests/Integration/FeatureMappingToInterfaceAndAbstractClassTest.php b/tests/Integration/FeatureMappingToInterfaceAndAbstractClassTest.php
new file mode 100644
index 0000000..e47410d
--- /dev/null
+++ b/tests/Integration/FeatureMappingToInterfaceAndAbstractClassTest.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Integration;
+
+use JsonMapper\Handler\FactoryRegistry;
+use JsonMapper\Handler\PropertyMapper;
+use JsonMapper\JsonMapperBuilder;
+use JsonMapper\Tests\Implementation\Models\AbstractShape;
+use JsonMapper\Tests\Implementation\Models\IShape;
+use JsonMapper\Tests\Implementation\Models\ShapeInstanceFactory;
+use JsonMapper\Tests\Implementation\Models\Square;
+use JsonMapper\Tests\Implementation\Models\Wrappers\AbstractShapeWrapper;
+use JsonMapper\Tests\Implementation\Models\Wrappers\IShapeAware;
+use JsonMapper\Tests\Implementation\Models\Wrappers\IShapeWrapper;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversNothing
+ */
+class FeatureMappingToInterfaceAndAbstractClassTest extends TestCase
+{
+    /**
+     * @dataProvider nonInstantiableTypes
+     */
+    public function testItCanMapToAnInterfaceType(IShapeAware $object, string $className): void
+    {
+        $interfaceResolver = new FactoryRegistry();
+        $interfaceResolver->addFactory($className, new ShapeInstanceFactory());
+        $mapper = JsonMapperBuilder::new()
+            ->withDocBlockAnnotationsMiddleware()
+            ->withNamespaceResolverMiddleware()
+            ->withPropertyMapper(new PropertyMapper(null, $interfaceResolver))
+            ->build();
+
+        $mapper->mapObjectFromString('{"shape": {"type": "square", "width": 5, "length": 6}}', $object);
+
+        self::assertInstanceOf(Square::class, $object->shape);
+        self::assertEquals(5, $object->shape->width);
+        self::assertEquals(6, $object->shape->length);
+    }
+
+    public function nonInstantiableTypes(): array
+    {
+        return [
+            'interface' => [new IShapeWrapper(), IShape::class],
+            'abstract' => [new AbstractShapeWrapper(), AbstractShape::class]
+        ];
+    }
+}
diff --git a/tests/Unit/Handler/ClassFactoryRegistryTest.php b/tests/Unit/Handler/FactoryRegistryTest.php
similarity index 70%
rename from tests/Unit/Handler/ClassFactoryRegistryTest.php
rename to tests/Unit/Handler/FactoryRegistryTest.php
index 0cdfed9..8558062 100644
--- a/tests/Unit/Handler/ClassFactoryRegistryTest.php
+++ b/tests/Unit/Handler/FactoryRegistryTest.php
@@ -2,42 +2,20 @@
 
 declare(strict_types=1);
 
-namespace JsonMapper\Tests\Handler;
+namespace JsonMapper\Tests\Unit\Handler;
 
 use JsonMapper\Exception\ClassFactoryException;
-use JsonMapper\Handler\ClassFactoryRegistry;
+use JsonMapper\Handler\FactoryRegistry;
 use PHPUnit\Framework\TestCase;
 
-class ClassFactoryRegistryTest extends TestCase
+class FactoryRegistryTest extends TestCase
 {
     /**
-     * @covers \JsonMapper\Handler\ClassFactoryRegistry
-     */
-    public function testLoadNativePhpClassFactoriesAddsFactoriesForNativeClasses(): void
-    {
-        $classFactoryRegistry = new ClassFactoryRegistry();
-        $classFactoryRegistry->loadNativePhpClassFactories();
-
-        self::assertTrue($classFactoryRegistry->hasFactory(\DateTime::class));
-        self::assertTrue($classFactoryRegistry->hasFactory(\DateTimeImmutable::class));
-        self::assertTrue($classFactoryRegistry->hasFactory(\stdClass::class));
-        self::assertEquals(new \DateTime('today'), $classFactoryRegistry->create(\DateTime::class, 'today'));
-        self::assertEquals(
-            new \DateTimeImmutable('today'),
-            $classFactoryRegistry->create(\DateTimeImmutable::class, 'today')
-        );
-        self::assertEquals(
-            (object) ['one' => 1, 'two' => 2],
-            $classFactoryRegistry->create(\stdClass::class, ['one' => 1, 'two' => 2])
-        );
-    }
-
-    /**
-     * @covers \JsonMapper\Handler\ClassFactoryRegistry
+     * @covers \JsonMapper\Handler\FactoryRegistry
      */
     public function testAddFactoryAddsFactory(): void
     {
-        $classFactoryRegistry = new ClassFactoryRegistry();
+        $classFactoryRegistry = new FactoryRegistry();
         $classFactoryRegistry->addFactory(__CLASS__, static function () {
         });
 
@@ -45,21 +23,21 @@ class ClassFactoryRegistryTest extends TestCase
     }
 
     /**
-     * @covers \JsonMapper\Handler\ClassFactoryRegistry
+     * @covers \JsonMapper\Handler\FactoryRegistry
      */
     public function testHasFactoryReturnsFalseWhenNoFactoryRegistered(): void
     {
-        $classFactoryRegistry = new ClassFactoryRegistry();
+        $classFactoryRegistry = new FactoryRegistry();
 
         self::assertFalse($classFactoryRegistry->hasFactory(__CLASS__));
     }
 
     /**
-     * @covers \JsonMapper\Handler\ClassFactoryRegistry
+     * @covers \JsonMapper\Handler\FactoryRegistry
      */
     public function testAddFactoryThrowsExceptionWhenDuplicateClassNameIsAdded(): void
     {
-        $classFactoryRegistry = new ClassFactoryRegistry();
+        $classFactoryRegistry = new FactoryRegistry();
         $classFactoryRegistry->addFactory(__CLASS__, static function () {
         });
 
@@ -70,11 +48,11 @@ class ClassFactoryRegistryTest extends TestCase
     }
 
     /**
-     * @covers \JsonMapper\Handler\ClassFactoryRegistry
+     * @covers \JsonMapper\Handler\FactoryRegistry
      */
     public function testCreateReturnsValueFromCallable(): void
     {
-        $classFactoryRegistry = new ClassFactoryRegistry();
+        $classFactoryRegistry = new FactoryRegistry();
         $object = new \stdClass();
         $classFactoryRegistry->addFactory(__CLASS__, static function () use ($object) {
             return $object;
@@ -84,11 +62,11 @@ class ClassFactoryRegistryTest extends TestCase
     }
 
     /**
-     * @covers \JsonMapper\Handler\ClassFactoryRegistry
+     * @covers \JsonMapper\Handler\FactoryRegistry
      */
     public function testCreateCanHandleLeadingSlash(): void
     {
-        $classFactoryRegistry = new ClassFactoryRegistry();
+        $classFactoryRegistry = new FactoryRegistry();
         $object = new \stdClass();
         $classFactoryRegistry->addFactory(\DateTimeImmutable::class, static function () use ($object) {
             return $object;
@@ -98,14 +76,35 @@ class ClassFactoryRegistryTest extends TestCase
     }
 
     /**
-     * @covers \JsonMapper\Handler\ClassFactoryRegistry
+     * @covers \JsonMapper\Handler\FactoryRegistry
      */
     public function testCreateThrowsExceptionForMissingFactory(): void
     {
-        $classFactoryRegistry = new ClassFactoryRegistry();
+        $classFactoryRegistry = new FactoryRegistry();
 
         $this->expectExceptionObject(ClassFactoryException::forMissingClassname(__CLASS__));
 
         $classFactoryRegistry->create(__CLASS__, new \stdClass());
     }
+
+    /**
+     * @covers \JsonMapper\Handler\FactoryRegistry
+     */
+    public function testWithNativePhpClassesAddedAddsFactoriesForNativeClasses(): void
+    {
+        $classFactoryRegistry = FactoryRegistry::WithNativePhpClassesAdded();
+
+        self::assertTrue($classFactoryRegistry->hasFactory(\DateTime::class));
+        self::assertTrue($classFactoryRegistry->hasFactory(\DateTimeImmutable::class));
+        self::assertTrue($classFactoryRegistry->hasFactory(\stdClass::class));
+        self::assertEquals(new \DateTime('today'), $classFactoryRegistry->create(\DateTime::class, 'today'));
+        self::assertEquals(
+            new \DateTimeImmutable('today'),
+            $classFactoryRegistry->create(\DateTimeImmutable::class, 'today')
+        );
+        self::assertEquals(
+            (object) ['one' => 1, 'two' => 2],
+            $classFactoryRegistry->create(\stdClass::class, ['one' => 1, 'two' => 2])
+        );
+    }
 }
diff --git a/tests/Unit/Handler/PropertyMapperTest.php b/tests/Unit/Handler/PropertyMapperTest.php
index f67844e..861b933 100644
--- a/tests/Unit/Handler/PropertyMapperTest.php
+++ b/tests/Unit/Handler/PropertyMapperTest.php
@@ -7,14 +7,18 @@ namespace JsonMapper\Tests\Unit\Handler;
 use JsonMapper\Builders\PropertyBuilder;
 use JsonMapper\Cache\NullCache;
 use JsonMapper\Enums\Visibility;
-use JsonMapper\Handler\ClassFactoryRegistry;
+use JsonMapper\Handler\FactoryRegistry;
 use JsonMapper\Handler\PropertyMapper;
 use JsonMapper\JsonMapperFactory;
 use JsonMapper\JsonMapperInterface;
 use JsonMapper\Middleware\DocBlockAnnotations;
 use JsonMapper\Tests\Implementation\ComplexObject;
+use JsonMapper\Tests\Implementation\Models\IShape;
+use JsonMapper\Tests\Implementation\Models\ShapeInstanceFactory;
+use JsonMapper\Tests\Implementation\Models\Square;
 use JsonMapper\Tests\Implementation\Models\User;
 use JsonMapper\Tests\Implementation\Models\UserWithConstructor;
+use JsonMapper\Tests\Implementation\Models\Wrappers\IShapeWrapper;
 use JsonMapper\Tests\Implementation\Popo;
 use JsonMapper\Tests\Implementation\PrivatePropertyWithoutSetter;
 use JsonMapper\Tests\Implementation\SimpleObject;
@@ -215,8 +219,7 @@ class PropertyMapperTest extends TestCase
         $json = (object) ['user' => (object) ['id' => 1234, 'name' => 'John Doe']];
         $object = new UserWithConstructorParent();
         $wrapped = new ObjectWrapper($object);
-        $classFactoryRegistry = new ClassFactoryRegistry();
-        $classFactoryRegistry->loadNativePhpClassFactories();
+        $classFactoryRegistry = FactoryRegistry::WithNativePhpClassesAdded();
         $classFactoryRegistry->addFactory(
             UserWithConstructor::class,
             static function ($params) {
@@ -250,8 +253,7 @@ class PropertyMapperTest extends TestCase
         ]];
         $object = new UserWithConstructorParent();
         $wrapped = new ObjectWrapper($object);
-        $classFactoryRegistry = new ClassFactoryRegistry();
-        $classFactoryRegistry->loadNativePhpClassFactories();
+        $classFactoryRegistry = FactoryRegistry::WithNativePhpClassesAdded();
         $classFactoryRegistry->addFactory(
             UserWithConstructor::class,
             static function ($params) {
@@ -289,8 +291,7 @@ class PropertyMapperTest extends TestCase
         ]];
         $object = new UserWithConstructorParent();
         $wrapped = new ObjectWrapper($object);
-        $classFactoryRegistry = new ClassFactoryRegistry();
-        $classFactoryRegistry->loadNativePhpClassFactories();
+        $classFactoryRegistry = FactoryRegistry::WithNativePhpClassesAdded();
         $classFactoryRegistry->addFactory(
             UserWithConstructor::class,
             static function ($params) {
@@ -580,6 +581,60 @@ class PropertyMapperTest extends TestCase
         self::assertEquals([1, 2, 3, 4, 5], $object->getNumbers());
     }
 
+    /**
+     * @covers \JsonMapper\Handler\PropertyMapper
+     */
+    public function testItThrowsAnExceptionWhenInterfaceTypeCantBeCreated(): void
+    {
+        $json = (object) ['shape' => (object) ['type' => 'square', 'width' => 5, 'length' => 6]];
+        $object = new IShapeWrapper();
+        $wrapped = new ObjectWrapper($object);
+        $type = IShape::class;
+        $property = PropertyBuilder::new()
+            ->setName('shape')
+            ->setIsNullable(false)
+            ->setVisibility(Visibility::PUBLIC())
+            ->addType($type, false)
+            ->build();
+        $propertyMap = new PropertyMap();
+        $propertyMap->addProperty($property);
+
+        $propertyMapper = new PropertyMapper();
+        $jsonMapper = (new JsonMapperFactory())->create($propertyMapper, new DocBlockAnnotations(new NullCache()));
+
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionMessage("Unable to resolve un-instantiable {$type} as no factory was registered");
+        $propertyMapper->__invoke($json, $wrapped, $propertyMap, $jsonMapper);
+    }
+
+    /**
+     * @covers \JsonMapper\Handler\PropertyMapper
+     */
+    public function testItCanMapInterfaceType(): void
+    {
+        $json = (object) ['shape' => (object) ['type' => 'square', 'width' => 5, 'length' => 6]];
+        $object = new IShapeWrapper();
+        $wrapped = new ObjectWrapper($object);
+        $type = IShape::class;
+        $property = PropertyBuilder::new()
+            ->setName('shape')
+            ->setIsNullable(false)
+            ->setVisibility(Visibility::PUBLIC())
+            ->addType($type, false)
+            ->build();
+        $propertyMap = new PropertyMap();
+        $propertyMap->addProperty($property);
+        $nonInstantiableTypeResolver = new FactoryRegistry();
+        $nonInstantiableTypeResolver->addFactory(IShape::class, new ShapeInstanceFactory());
+
+        $propertyMapper = new PropertyMapper(null, $nonInstantiableTypeResolver);
+        $jsonMapper = (new JsonMapperFactory())->create($propertyMapper, new DocBlockAnnotations(new NullCache()));
+
+        $propertyMapper->__invoke($json, $wrapped, $propertyMap, $jsonMapper);
+
+        self::assertEquals(new Square(5, 6), $object->shape);
+    }
+
     public function scalarValueDataTypes(): array
     {
         return [