You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2020/10/12 11:56:52 UTC

[sling-org-apache-sling-graphql-core] branch master updated: SLING-9796 - Add support for using Unions

This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git


The following commit(s) were added to refs/heads/master by this push:
     new 16919c9  SLING-9796 - Add support for using Unions
16919c9 is described below

commit 16919c929e31bef083dd5d9c4314a16f9ada5666
Author: Radu Cotescu <17...@users.noreply.github.com>
AuthorDate: Mon Oct 12 13:56:44 2020 +0200

    SLING-9796 - Add support for using Unions
    
    * applied a slightly modified patch provided by Adrian Kozma
    
    Co-authored-by: Radu Cotescu <ra...@apache.org>
    Co-authored-by: Adrian Kozma <ko...@adobe.com>
---
 README.md                                          |  57 +++++++-
 .../sling/graphql/api/SlingTypeResolver.java       |  41 ++++++
 .../graphql/api/SlingTypeResolverEnvironment.java  |  65 ++++++++++
 .../graphql/core/engine/DefaultQueryExecutor.java  |  68 ++++++++--
 .../core/engine/SlingTypeResolverSelector.java     | 144 +++++++++++++++++++++
 .../core/engine/SlingTypeResolverWrapper.java      |  54 ++++++++
 .../engine/TypeResolverEnvironmentWrapper.java     |  73 +++++++++++
 .../example/resolvers/DoNothingTypeResolver.java   |  36 ++++++
 .../core/engine/DefaultQueryExecutorTest.java      |  42 +++++-
 .../core/engine/IntrospectionQueryTest.java        |  24 +++-
 .../graphql/core/engine/ResourceQueryTestBase.java |   1 +
 .../SlingTypeResolverNameValidationTest.java       |  65 ++++++++++
 .../core/engine/SlingTypeResolverSelectorTest.java |  85 ++++++++++++
 .../graphql/core/mocks/DummyTypeResolver.java      |  32 +++++
 .../apache/sling/graphql/core/mocks/TestUtil.java  |  13 ++
 .../graphql/core/mocks/TypeSlingResourceDTO.java   |  37 ++++++
 .../sling/graphql/core/mocks/TypeTestDTO.java      |  50 +++++++
 .../UnionTypeResolver.java}                        |  33 ++---
 .../core/schema/SchemaDescriptionsTest.java        |   5 +
 ...{test-schema.txt => failing-fetcher-schema.txt} |  31 +----
 ...schema.txt => failing-type-resolver-schema.txt} |  33 +----
 src/test/resources/test-schema.txt                 |  19 ++-
 22 files changed, 919 insertions(+), 89 deletions(-)

diff --git a/README.md b/README.md
index b6159b6..f71a423 100644
--- a/README.md
+++ b/README.md
@@ -100,11 +100,66 @@ The names of those `SlingDataFetcher` services are in the form
     <namespace>/<name>
 
 The `sling/` namespace is reserved for `SlingDataFetcher` services
-which hava Java package names that start with `org.apache.sling`.
+which have Java package names that start with `org.apache.sling`.
 
 The `<options>` and `<source>` arguments of the directive can be used by the
 `SlingDataFetcher` services to influence their behavior.
 
+<<<<<<< HEAD
+=======
+### Scripted SlingDataFetchers
+
+Besides Java, `SlingDataFetcher` scripts can be written in any scripting language that supported by the Sling instance's configuration.
+
+Here's an example from the test code. The schema contains the following statement:
+
+    scriptedFetcher (testing : String) : Test @fetcher(name:"scripted/example")
+
+And here's the data fetcher code:
+
+```javascript
+var result = { 
+    boolValue: true,
+    resourcePath: "From the test script: " + resource.path,
+    testingArgument: environment.getArgument("testing"),
+    anotherValue: 450 + 1
+};
+
+result;
+```
+    
+The data fetcher provider then looks for a script that handles the `graphql/fetchers/scripted/example` resource type with a `fetcher`script name. `graphql/fetchers`is a prefix (hardcoded for now) and `scripted/example` comes from the above schema's `@fetcher` directive.
+
+In that test, the `/graphql/fetchers/scripted/example/fetcher.js` shown above resolves with those requirements, it is executed to retrieve the requested data. That execution happens with a context consisting of the current `SlingDataFetcherEnvironment` under the `environment` key, and the current Sling Resource under the `resource` key, both used in this test script.
+
+## SlingTypeResolver selection with Schema Directives
+
+The GraphQL schemas used by this module can be enhanced using
+[schema directives](http://spec.graphql.org/June2018/#sec-Language.Directives)
+(see also the [Apollo docs](https://www.apollographql.com/docs/graphql-tools/schema-directives/) for how those work)
+that select specific `SlingTypeResolver` services to return the appropriate GraphQL object type using Unions.
+
+Here's a simple example, the test code has more:
+
+    # This directive maps the corresponding type resolver to a given Union
+    directive @resolver(
+        name: String, 
+        options: String = "", 
+        source: String = ""
+    ) on UNION
+
+    union TestUnion @resolver(name : "test/resolver", source : "TestUnion") = Type_1 | Type_2 | Type_3 | Type_4
+
+The names of those `SlingTypeResolver` services are in the form
+
+    <namespace>/<name>
+
+The `sling/` namespace is reserved for `SlingTypeResolver` services
+which have Java package names that start with `org.apache.sling`.
+
+The `<options>` and `<source>` arguments of the directive can be used by the
+`SlingTypeResolver` services to influence their behavior.
+
 ## Caching: Persisted queries API
 
 No matter how you decide to create your Sling GraphQL endpoints, you have the option to allow GraphQL clients to use persisted queries.
diff --git a/src/main/java/org/apache/sling/graphql/api/SlingTypeResolver.java b/src/main/java/org/apache/sling/graphql/api/SlingTypeResolver.java
new file mode 100644
index 0000000..19dbc94
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/api/SlingTypeResolver.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.graphql.api;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * Resolves the GraphQL object type of a given result item. Services must be registered with a {@link #NAME_SERVICE_PROPERTY} property
+ * with a unique value that's matched with the corresponding {@code @directive} in the GraphQL Schema.
+ */
+@ConsumerType
+public interface SlingTypeResolver<T> {
+
+    /**
+     * Defines the service registration property with which all {@link SlingTypeResolver} services have to be registered. The value should
+     * be namespaced, with namespaces being delimited by the "/" character.
+     */
+    String NAME_SERVICE_PROPERTY = "name";
+
+    @Nullable
+    T getType(@NotNull SlingTypeResolverEnvironment e);
+}
diff --git a/src/main/java/org/apache/sling/graphql/api/SlingTypeResolverEnvironment.java b/src/main/java/org/apache/sling/graphql/api/SlingTypeResolverEnvironment.java
new file mode 100644
index 0000000..9c52196
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/api/SlingTypeResolverEnvironment.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.graphql.api;
+
+import org.apache.sling.api.resource.Resource;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Provides contextual information to the {@link SlingTypeResolver}
+ */
+@SuppressWarnings("TypeParameterUnusedInFormals")
+
+@ProviderType
+public interface SlingTypeResolverEnvironment<T> {
+
+    /**
+     * @return the current Sling resource
+     */
+    @Nullable
+    Resource getCurrentResource();
+
+    /**
+     * @return the options, if set by the schema directive
+     */
+    @Nullable
+    String getResolverOptions();
+
+    /**
+     * @return the source, if set by the schema directive
+     */
+    @Nullable
+    String getResolverSource();
+
+    /**
+     * @return the GraphQL result item
+     */
+    @Nullable
+    Object getObject();
+
+    /**
+     * @param name the type name
+     * @return the GraphQL Object Type
+     */
+    @Nullable
+    T getObjectType(@NotNull String name);
+}
diff --git a/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java b/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
index b6ecfea..09fafd5 100644
--- a/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
+++ b/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
@@ -23,12 +23,16 @@ import java.util.Map;
 
 import javax.script.ScriptException;
 
+import graphql.language.UnionTypeDefinition;
+import graphql.schema.GraphQLObjectType;
+import graphql.schema.TypeResolver;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.graphql.api.SchemaProvider;
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingGraphQLException;
 import org.apache.sling.graphql.api.engine.QueryExecutor;
 import org.apache.sling.graphql.api.engine.ValidationResult;
+import org.apache.sling.graphql.api.SlingTypeResolver;
 import org.apache.sling.graphql.core.scalars.SlingScalarsProvider;
 import org.apache.sling.graphql.core.schema.RankedSchemaProviders;
 import org.jetbrains.annotations.NotNull;
@@ -70,6 +74,11 @@ public class DefaultQueryExecutor implements QueryExecutor {
     public static final String FETCHER_OPTIONS = "options";
     public static final String FETCHER_SOURCE = "source";
 
+    public static final String RESOLVER_DIRECTIVE = "resolver";
+    public static final String RESOLVER_NAME = "name";
+    public static final String RESOLVER_OPTIONS = "options";
+    public static final String RESOLVER_SOURCE = "source";
+
     @Reference
     private RankedSchemaProviders schemaProvider;
 
@@ -77,6 +86,9 @@ public class DefaultQueryExecutor implements QueryExecutor {
     private SlingDataFetcherSelector dataFetcherSelector;
 
     @Reference
+    private SlingTypeResolverSelector typeResolverSelector;
+
+    @Reference
     private SlingScalarsProvider scalarsProvider;
 
     @Override
@@ -85,8 +97,7 @@ public class DefaultQueryExecutor implements QueryExecutor {
         try {
             String schemaDef = prepareSchemaDefinition(schemaProvider, queryResource, selectors);
             LOGGER.debug("Resource {} maps to GQL schema {}", queryResource.getPath(), schemaDef);
-            final GraphQLSchema schema =
-                    buildSchema(schemaDef, dataFetcherSelector, scalarsProvider, queryResource);
+            final GraphQLSchema schema = buildSchema(schemaDef, queryResource);
             ExecutionInput executionInput = ExecutionInput.newExecutionInput()
                     .query(query)
                     .variables(variables)
@@ -118,7 +129,7 @@ public class DefaultQueryExecutor implements QueryExecutor {
         try {
             schemaDef = prepareSchemaDefinition(schemaProvider, queryResource, selectors);
             LOGGER.debug("Resource {} maps to GQL schema {}", queryResource.getPath(), schemaDef);
-            final GraphQLSchema schema = buildSchema(schemaDef, dataFetcherSelector, scalarsProvider, queryResource);
+            final GraphQLSchema schema = buildSchema(schemaDef, queryResource);
             final GraphQL graphQL = GraphQL.newGraphQL(schema).build();
             LOGGER.debug("Executing query\n[{}]\nat [{}] with variables [{}]", query, queryResource.getPath(), variables);
             ExecutionInput ei = ExecutionInput.newExecutionInput()
@@ -147,24 +158,22 @@ public class DefaultQueryExecutor implements QueryExecutor {
         }
     }
 
-    private GraphQLSchema buildSchema(String sdl, SlingDataFetcherSelector fetchers, SlingScalarsProvider scalarsProvider,
-                                             Resource currentResource) {
+    private GraphQLSchema buildSchema(String sdl, Resource currentResource) {
         TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
         Iterable<GraphQLScalarType> scalars = scalarsProvider.getCustomScalars(typeRegistry.scalars());
-        RuntimeWiring runtimeWiring = buildWiring(typeRegistry, fetchers, scalars, currentResource);
+        RuntimeWiring runtimeWiring = buildWiring(typeRegistry, scalars, currentResource);
         SchemaGenerator schemaGenerator = new SchemaGenerator();
         return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
     }
 
-    private RuntimeWiring buildWiring(TypeDefinitionRegistry typeRegistry, SlingDataFetcherSelector fetchers,
-                                             Iterable<GraphQLScalarType> scalars, Resource r) {
+    private RuntimeWiring buildWiring(TypeDefinitionRegistry typeRegistry, Iterable<GraphQLScalarType> scalars, Resource r) {
         List<ObjectTypeDefinition> types = typeRegistry.getTypes(ObjectTypeDefinition.class);
         RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
         for (ObjectTypeDefinition type : types) {
             builder.type(type.getName(), typeWiring -> {
                 for (FieldDefinition field : type.getFieldDefinitions()) {
                     try {
-                        DataFetcher<Object> fetcher = getDataFetcher(field, fetchers, r);
+                        DataFetcher<Object> fetcher = getDataFetcher(field, r);
                         if (fetcher != null) {
                             typeWiring.dataFetcher(field.getName(), fetcher);
                         }
@@ -178,6 +187,20 @@ public class DefaultQueryExecutor implements QueryExecutor {
             });
         }
         scalars.forEach(builder::scalar);
+
+        List<UnionTypeDefinition> unionTypes = typeRegistry.getTypes(UnionTypeDefinition.class);
+        for (UnionTypeDefinition type : unionTypes) {
+            try {
+                TypeResolver resolver = getTypeResolver(type, r);
+                if (resolver != null) {
+                    builder.type(type.getName(), typeWriting -> typeWriting.typeResolver(resolver));
+                }
+            } catch (SlingGraphQLException e) {
+                throw e;
+            } catch(Exception e) {
+                throw new SlingGraphQLException("Exception while building wiring.", e);
+            }
+        }
         return builder.build();
     }
 
@@ -197,7 +220,15 @@ public class DefaultQueryExecutor implements QueryExecutor {
                 name, SlingDataFetcherSelector.FETCHER_NAME_PATTERN));
     }
 
-    private DataFetcher<Object> getDataFetcher(FieldDefinition field, SlingDataFetcherSelector fetchers, Resource currentResource)
+    private @NotNull String validateResolverName(String name) {
+        if (SlingTypeResolverSelector.nameMatchesPattern(name)) {
+            return name;
+        }
+        throw new SlingGraphQLException(String.format("Invalid type resolver name %s, does not match %s",
+                name, SlingTypeResolverSelector.RESOLVER_NAME_PATTERN));
+    }
+
+    private DataFetcher<Object> getDataFetcher(FieldDefinition field, Resource currentResource)
             {
         DataFetcher<Object> result = null;
         final Directive d =field.getDirective(FETCHER_DIRECTIVE);
@@ -205,7 +236,7 @@ public class DefaultQueryExecutor implements QueryExecutor {
             final String name = validateFetcherName(getDirectiveArgumentValue(d, FETCHER_NAME));
             final String options = getDirectiveArgumentValue(d, FETCHER_OPTIONS);
             final String source = getDirectiveArgumentValue(d, FETCHER_SOURCE);
-            SlingDataFetcher<Object> f = fetchers.getSlingFetcher(name);
+            SlingDataFetcher<Object> f = dataFetcherSelector.getSlingFetcher(name);
             if(f != null) {
                 result = new SlingDataFetcherWrapper<>(f, currentResource, options, source);
             }
@@ -213,6 +244,21 @@ public class DefaultQueryExecutor implements QueryExecutor {
         return result;
     }
 
+    private TypeResolver getTypeResolver(UnionTypeDefinition typeDefinition, Resource currentResource) {
+        TypeResolver resolver = null;
+        final Directive d = typeDefinition.getDirective(RESOLVER_DIRECTIVE);
+        if(d != null) {
+            final String name = validateResolverName(getDirectiveArgumentValue(d, RESOLVER_NAME));
+            final String options = getDirectiveArgumentValue(d, RESOLVER_OPTIONS);
+            final String source = getDirectiveArgumentValue(d, RESOLVER_SOURCE);
+            SlingTypeResolver<Object> r = typeResolverSelector.getSlingTypeResolver(name);
+            if(r != null) {
+                resolver = new SlingTypeResolverWrapper(r, currentResource, options, source);
+            }
+        }
+        return resolver;
+    }
+
     private @Nullable String prepareSchemaDefinition(@NotNull SchemaProvider schemaProvider,
                                                             @NotNull org.apache.sling.api.resource.Resource resource,
                                                             @NotNull String[] selectors) throws ScriptException {
diff --git a/src/main/java/org/apache/sling/graphql/core/engine/SlingTypeResolverSelector.java b/src/main/java/org/apache/sling/graphql/core/engine/SlingTypeResolverSelector.java
new file mode 100644
index 0000000..b59cd62
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/engine/SlingTypeResolverSelector.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.graphql.core.engine;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.graphql.api.SlingTypeResolver;
+import org.apache.sling.graphql.core.osgi.ServiceReferenceObjectTuple;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Selects a SlingTypeProvider used to get the corresponding object type,
+ * based on a name specified by a GraphQL schema directive.
+ */
+@Component(service = SlingTypeResolverSelector.class)
+public class SlingTypeResolverSelector {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(SlingTypeResolverSelector.class);
+    static final Pattern RESOLVER_NAME_PATTERN = Pattern.compile("\\w+(/\\w+)+");
+
+    private final Map<String, TreeSet<ServiceReferenceObjectTuple<SlingTypeResolver<Object>>>> typeResolvers = new HashMap<>();
+
+    /**
+     * Resolvers which have a name starting with this prefix must be
+     * under the {#link RESERVED_PACKAGE_PREFIX} package.
+     */
+    public static final String RESERVED_NAME_PREFIX = "sling/";
+
+    /**
+     * Package name prefix for resolvers which have names starting
+     * with the {#link RESERVED_NAME_PREFIX}.
+     */
+    public static final String RESERVED_PACKAGE_PREFIX = "org.apache.sling.";
+
+    /**
+     * @return a SlingTypeResolver, or null if none available. First tries to get an
+     * OSGi SlingTypeResolver service, and if not found tries to find a scripted SlingTypeResolver.
+     */
+    @Nullable
+    public SlingTypeResolver<Object> getSlingTypeResolver(@NotNull String name) {
+        TreeSet<ServiceReferenceObjectTuple<SlingTypeResolver<Object>>> resolvers = typeResolvers.get(name);
+        if (resolvers != null && !resolvers.isEmpty()) {
+            return resolvers.last().getServiceObject();
+        }
+        return null;
+    }
+
+    private boolean hasValidName(@NotNull ServiceReference<SlingTypeResolver<Object>> serviceReference,
+                                 @NotNull SlingTypeResolver<Object> slingTypeResolver) {
+        String name = PropertiesUtil.toString(serviceReference.getProperty(SlingTypeResolver.NAME_SERVICE_PROPERTY), null);
+        if (StringUtils.isNotEmpty(name)) {
+            if (!nameMatchesPattern(name)) {
+                LOGGER.error("Invalid SlingTypeResolver {}: type resolver name is not namespaced (e.g. ns/myTypeResolver)",
+                        slingTypeResolver.getClass().getName());
+                return false;
+            }
+            if (name.startsWith(RESERVED_NAME_PREFIX)) {
+                final String className = slingTypeResolver.getClass().getName();
+                if (!slingTypeResolver.getClass().getName().startsWith(RESERVED_PACKAGE_PREFIX)) {
+                    LOGGER.error(
+                            "Invalid SlingTypeResolver {}: type resolver names starting with '{}' are reserved for Apache Sling Java " +
+                                    "packages",
+                            className, RESERVED_NAME_PREFIX);
+                    return false;
+                }
+            }
+        } else {
+            LOGGER.error("Invalid {} implementation: type resolver {} is missing the mandatory value for its {} service property.",
+                    SlingTypeResolver.class.getName(), slingTypeResolver.getClass().getName(), SlingTypeResolver.NAME_SERVICE_PROPERTY);
+            return false;
+        }
+        return true;
+    }
+
+    static boolean nameMatchesPattern(String name) {
+        if (StringUtils.isNotEmpty(name)) {
+            return RESOLVER_NAME_PATTERN.matcher(name).matches();
+        }
+        return false;
+    }
+
+    @Reference(
+            service = SlingTypeResolver.class,
+            cardinality = ReferenceCardinality.MULTIPLE,
+            policy = ReferencePolicy.DYNAMIC
+    )
+    private void bindSlingTypeResolver(ServiceReference<SlingTypeResolver<Object>> reference, SlingTypeResolver<Object> slingTypeResolver) {
+        if (hasValidName(reference, slingTypeResolver)) {
+            synchronized (typeResolvers) {
+                String name = (String) reference.getProperty(SlingTypeResolver.NAME_SERVICE_PROPERTY);
+                TreeSet<ServiceReferenceObjectTuple<SlingTypeResolver<Object>>> resolvers = typeResolvers.computeIfAbsent(name,
+                        key -> new TreeSet<>());
+                resolvers.add(new ServiceReferenceObjectTuple<>(reference, slingTypeResolver));
+            }
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private void unbindSlingTypeResolver(ServiceReference<SlingTypeResolver<Object>> reference) {
+        String name = (String) reference.getProperty(SlingTypeResolver.NAME_SERVICE_PROPERTY);
+        if (StringUtils.isNotEmpty(name)) {
+            synchronized (typeResolvers) {
+                TreeSet<ServiceReferenceObjectTuple<SlingTypeResolver<Object>>> resolvers = typeResolvers.get(name);
+                if (resolvers != null) {
+                    Optional<ServiceReferenceObjectTuple<SlingTypeResolver<Object>>> tupleToRemove =
+                            resolvers.stream().filter(tuple -> reference.equals(tuple.getServiceReference())).findFirst();
+                    tupleToRemove.ifPresent(resolvers::remove);
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/graphql/core/engine/SlingTypeResolverWrapper.java b/src/main/java/org/apache/sling/graphql/core/engine/SlingTypeResolverWrapper.java
new file mode 100644
index 0000000..a9976c6
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/engine/SlingTypeResolverWrapper.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.graphql.core.engine;
+
+import graphql.TypeResolutionEnvironment;
+import graphql.schema.GraphQLObjectType;
+import graphql.schema.TypeResolver;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.graphql.api.SlingTypeResolver;
+
+/**
+ * Wraps a SlingTypeResolver to make it usable by graphql-java
+ */
+class SlingTypeResolverWrapper implements TypeResolver {
+
+    private final SlingTypeResolver<Object> resolver;
+    private final Resource currentResource;
+    private final String options;
+    private final String source;
+
+    SlingTypeResolverWrapper(SlingTypeResolver<Object> resolver, Resource currentResource, String options,
+                             String source) {
+        this.resolver = resolver;
+        this.currentResource = currentResource;
+        this.options = options;
+        this.source = source;
+    }
+
+    @Override
+    public GraphQLObjectType getType(TypeResolutionEnvironment environment) {
+        Object r = resolver.getType(new TypeResolverEnvironmentWrapper(environment, currentResource, options, source));
+        if (r instanceof GraphQLObjectType) {
+            return (GraphQLObjectType) r;
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/sling/graphql/core/engine/TypeResolverEnvironmentWrapper.java b/src/main/java/org/apache/sling/graphql/core/engine/TypeResolverEnvironmentWrapper.java
new file mode 100644
index 0000000..a2a8583
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/engine/TypeResolverEnvironmentWrapper.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.graphql.core.engine;
+
+import graphql.TypeResolutionEnvironment;
+import graphql.schema.GraphQLObjectType;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.graphql.api.SlingTypeResolverEnvironment;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Wraps the graphql-java TypeResolverEnvironment to provide
+ * our own SlingTypeResolverEnvironment interface. This avoids
+ * having to expose the graphql-java APIs in our own API.
+ */
+class TypeResolverEnvironmentWrapper implements SlingTypeResolverEnvironment<GraphQLObjectType> {
+    private final TypeResolutionEnvironment env;
+    private final Resource currentResource;
+    private final String options;
+    private final String source;
+
+    TypeResolverEnvironmentWrapper(TypeResolutionEnvironment env, Resource currentResource, String options,
+                                   String source) {
+        this.env = env;
+        this.currentResource = currentResource;
+        this.options = options;
+        this.source = source;
+    }
+
+    @Override
+    public Resource getCurrentResource() {
+        return currentResource;
+    }
+
+    @Override
+    public String getResolverOptions() {
+        return options;
+    }
+
+    @Override
+    public String getResolverSource() {
+        return source;
+    }
+
+    @Override
+    public Object getObject() {
+        return env.getObject();
+    }
+
+    @Override
+    @Nullable
+    public GraphQLObjectType getObjectType(@NotNull String name) {
+        return env.getSchema().getObjectType(name);
+    }
+}
diff --git a/src/test/java/com/example/resolvers/DoNothingTypeResolver.java b/src/test/java/com/example/resolvers/DoNothingTypeResolver.java
new file mode 100644
index 0000000..8aaeafa
--- /dev/null
+++ b/src/test/java/com/example/resolvers/DoNothingTypeResolver.java
@@ -0,0 +1,36 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package com.example.resolvers;
+
+import org.apache.sling.graphql.api.SlingTypeResolver;
+import org.apache.sling.graphql.api.SlingTypeResolverEnvironment;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/** SlingTypeResolver used to test the requirement for sling/* fetcher
+ *  names to point to classes under the org.apache.sling package
+ */
+public class DoNothingTypeResolver implements SlingTypeResolver<Object> {
+
+    @Nullable
+    @Override
+    public Object getType(@NotNull SlingTypeResolverEnvironment e) {
+        return null;
+    }
+}
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java b/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
index 903f738..f9ab2d9 100644
--- a/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
@@ -18,19 +18,25 @@
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 package org.apache.sling.graphql.core.engine;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Dictionary;
 import java.util.Hashtable;
+import java.util.List;
 
 import org.apache.sling.graphql.api.SchemaProvider;
 import org.apache.sling.graphql.api.SlingGraphQLException;
 import org.apache.sling.graphql.api.engine.QueryExecutor;
 import org.apache.sling.graphql.api.engine.ValidationResult;
 import org.apache.sling.graphql.core.mocks.DigestDataFetcher;
+import org.apache.sling.graphql.core.mocks.DummyTypeResolver;
 import org.apache.sling.graphql.core.mocks.EchoDataFetcher;
 import org.apache.sling.graphql.core.mocks.FailingDataFetcher;
 import org.apache.sling.graphql.core.mocks.MockSchemaProvider;
 import org.apache.sling.graphql.core.mocks.TestUtil;
+import org.apache.sling.graphql.core.mocks.TypeSlingResourceDTO;
+import org.apache.sling.graphql.core.mocks.TypeTestDTO;
+import org.apache.sling.graphql.core.mocks.UnionTypeResolver;
 import org.junit.Test;
 import org.osgi.framework.Constants;
 import org.osgi.framework.ServiceRegistration;
@@ -47,10 +53,19 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public class DefaultQueryExecutorTest extends ResourceQueryTestBase {
+
     protected void setupAdditionalServices() {
         final Dictionary<String, Object> staticData = new Hashtable<>();
         staticData.put("test", true);
 
+        final Dictionary<String, Object> unionData = new Hashtable<>();
+        final List<Object> items = new ArrayList<>();
+        items.add(new TypeTestDTO(true, false, "path/to/resource", "1, 2, 3"));
+        items.add(new TypeSlingResourceDTO(resource.getPath(), resource.getResourceType()));
+        unionData.put("items", items);
+
+        TestUtil.registerSlingTypeResolver(context.bundleContext(), "union/resolver", new UnionTypeResolver());
+        TestUtil.registerSlingDataFetcher(context.bundleContext(), "union/fetcher", new EchoDataFetcher(unionData));
         TestUtil.registerSlingDataFetcher(context.bundleContext(), "echoNS/echo", new EchoDataFetcher(null));
         TestUtil.registerSlingDataFetcher(context.bundleContext(), "failure/fail", new FailingDataFetcher());
         TestUtil.registerSlingDataFetcher(context.bundleContext(), "test/static", new EchoDataFetcher(staticData));
@@ -130,17 +145,40 @@ public class DefaultQueryExecutorTest extends ResourceQueryTestBase {
 
     @Test
     public void invalidFetcherNamesTest() {
-        context.registerService(SchemaProvider.class, new MockSchemaProvider("failing-schema"), Constants.SERVICE_RANKING,
+        context.registerService(SchemaProvider.class, new MockSchemaProvider("failing-fetcher-schema"), Constants.SERVICE_RANKING,
                 Integer.MAX_VALUE);
         final ServiceRegistration<?> reg = TestUtil.registerSlingDataFetcher(context.bundleContext(), "missingSlash", new EchoDataFetcher(42));
         try {
             queryJSON("{ currentResource { missingSlash } }", new String[] {});
             fail("Expected query to fail");
         } catch(Exception e) {
-            TestUtil.assertNestedException(e, SlingGraphQLException.class, "does not match");
+            TestUtil.assertNestedException(e, SlingGraphQLException.class, "Invalid fetcher name missingSlash");
         } finally {
             reg.unregister();
         }
     }
 
+    @Test
+    public void invalidTypeResolverNamesTest() {
+        context.registerService(SchemaProvider.class, new MockSchemaProvider("failing-type-resolver-schema"), Constants.SERVICE_RANKING,
+                Integer.MAX_VALUE);
+        final ServiceRegistration<?> reg = TestUtil.registerSlingTypeResolver(context.bundleContext(), "missingSlash",
+                new DummyTypeResolver());
+        try {
+            queryJSON("{ currentResource { missingSlash } }", new String[] {});
+            fail("Expected query to fail");
+        } catch(Exception e) {
+            TestUtil.assertNestedException(e, SlingGraphQLException.class, "Invalid type resolver name missingSlash");
+        } finally {
+            reg.unregister();
+        }
+    }
+
+    @Test
+    public void unionFetcherTest() throws Exception {
+        final String json = queryJSON("{ unionFetcher { items { ... on Test { testingArgument }  ... on SlingResource { path }} } }");
+        assertThat(json, hasJsonPath("$.data.unionFetcher"));
+        assertThat(json, hasJsonPath("$.data.unionFetcher.items[0].testingArgument", equalTo("1, 2, 3")));
+        assertThat(json, hasJsonPath("$.data.unionFetcher.items[1].path", equalTo(resource.getPath())));
+    }
 }
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/IntrospectionQueryTest.java b/src/test/java/org/apache/sling/graphql/core/engine/IntrospectionQueryTest.java
index 4c4d476..227a49f 100644
--- a/src/test/java/org/apache/sling/graphql/core/engine/IntrospectionQueryTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/engine/IntrospectionQueryTest.java
@@ -18,15 +18,37 @@
  */
 package org.apache.sling.graphql.core.engine;
 
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
 import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
 
 import static org.junit.Assert.assertThat;
 import static org.hamcrest.Matchers.contains;
 
+import org.apache.sling.graphql.core.mocks.EchoDataFetcher;
+import org.apache.sling.graphql.core.mocks.TestUtil;
+import org.apache.sling.graphql.core.mocks.TypeSlingResourceDTO;
+import org.apache.sling.graphql.core.mocks.TypeTestDTO;
+import org.apache.sling.graphql.core.mocks.UnionTypeResolver;
 import org.junit.Test;
 
 public class IntrospectionQueryTest extends ResourceQueryTestBase {
-    
+
+    @Override
+    protected void setupAdditionalServices() {
+        final Dictionary<String, Object> unionData = new Hashtable<>();
+        final List<Object> items = new ArrayList<>();
+        items.add(new TypeTestDTO(true, false, "path/to/resource", "1, 2, 3"));
+        items.add(new TypeSlingResourceDTO(resource.getPath(), resource.getResourceType()));
+        unionData.put("items", items);
+
+        TestUtil.registerSlingTypeResolver(context.bundleContext(), "union/resolver", new UnionTypeResolver());
+        TestUtil.registerSlingDataFetcher(context.bundleContext(), "union/fetcher", new EchoDataFetcher(unionData));
+    }
+
     @Test
     public void schemaIntrospectionTest() throws Exception {
         final String json = queryJSON("{ __schema { types { name } directives { description }}}");
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/ResourceQueryTestBase.java b/src/test/java/org/apache/sling/graphql/core/engine/ResourceQueryTestBase.java
index eb9090d..3fa7c0f 100644
--- a/src/test/java/org/apache/sling/graphql/core/engine/ResourceQueryTestBase.java
+++ b/src/test/java/org/apache/sling/graphql/core/engine/ResourceQueryTestBase.java
@@ -67,6 +67,7 @@ public abstract class ResourceQueryTestBase {
         context.bundleContext().registerService(ServletResolver.class, servletResolver, null);
 
         context.registerInjectActivateService(new SlingDataFetcherSelector());
+        context.registerInjectActivateService(new SlingTypeResolverSelector());
         context.registerInjectActivateService(new SlingScalarsProvider());
         context.registerInjectActivateService(new RankedSchemaProviders());
         context.registerInjectActivateService(new DefaultQueryExecutor());
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/SlingTypeResolverNameValidationTest.java b/src/test/java/org/apache/sling/graphql/core/engine/SlingTypeResolverNameValidationTest.java
new file mode 100644
index 0000000..fbe8273
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/core/engine/SlingTypeResolverNameValidationTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.graphql.core.engine;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Parameterized.class)
+public class SlingTypeResolverNameValidationTest {
+
+    @Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        final List<Object[]> result = new ArrayList<>();
+        result.add(new Object[]{"with/slash", true});
+        result.add(new Object[]{"missingSlash", false});
+        result.add(new Object[]{"", false});
+        result.add(new Object[]{"one/two/three", true});
+        result.add(new Object[]{"one/two/three/four_and/five_and_451_six_6", true});
+        result.add(new Object[]{"uno/1/x42", true});
+        result.add(new Object[]{"uno_due/tre", true});
+        result.add(new Object[]{"the:colon/bad", false});
+        result.add(new Object[]{"/startingslash", false});
+        result.add(new Object[]{"/starting/ending", false});
+        return result;
+    }
+
+    private final String name;
+    private final boolean expectValid;
+
+    public SlingTypeResolverNameValidationTest(String name, Boolean expectValid) {
+        this.name = name;
+        this.expectValid = expectValid;
+    }
+
+    @Test
+    public void testValidation() {
+        final String msg = String.format("Expecting '%s' to be %s", name, expectValid ? "valid" : "invalid");
+        assertEquals(msg, SlingTypeResolverSelector.nameMatchesPattern(name), expectValid);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/SlingTypeResolverSelectorTest.java b/src/test/java/org/apache/sling/graphql/core/engine/SlingTypeResolverSelectorTest.java
new file mode 100644
index 0000000..db0f254
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/core/engine/SlingTypeResolverSelectorTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.graphql.core.engine;
+
+import org.apache.sling.graphql.api.SlingTypeResolver;
+import org.apache.sling.graphql.api.SlingTypeResolverEnvironment;
+import org.apache.sling.graphql.core.mocks.DummyTypeResolver;
+import org.apache.sling.graphql.core.mocks.TestUtil;
+import org.apache.sling.graphql.core.mocks.UnionTypeResolver;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import com.example.resolvers.DoNothingTypeResolver;
+
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+public class SlingTypeResolverSelectorTest {
+
+    @Rule
+    public final OsgiContext context = new OsgiContext();
+
+    private SlingTypeResolverSelector selector;
+
+    @Before
+    public void setup() {
+        context.registerInjectActivateService(new SlingTypeResolverSelector());
+        selector = context.getService(SlingTypeResolverSelector.class);
+
+        TestUtil.registerSlingTypeResolver(context.bundleContext(), "sling/union", new UnionTypeResolver());
+        TestUtil.registerSlingTypeResolver(context.bundleContext(), "sling/shouldFail", new DoNothingTypeResolver());
+        TestUtil.registerSlingTypeResolver(context.bundleContext(), "example/ok", new DoNothingTypeResolver());
+        TestUtil.registerSlingTypeResolver(context.bundleContext(), "sling/duplicate", 1, new DummyTypeResolver());
+        TestUtil.registerSlingTypeResolver(context.bundleContext(), "sling/duplicate", 0, new UnionTypeResolver());
+    }
+
+    @Test
+    public void acceptableName() {
+        final SlingTypeResolver<Object> sdf = selector.getSlingTypeResolver("example/ok");
+        assertThat(sdf, not(nullValue()));
+    }
+
+    @Test
+    public void reservedNameOk() {
+        final SlingTypeResolver<Object> sdf = selector.getSlingTypeResolver("sling/union");
+        assertThat(sdf, not(nullValue()));
+    }
+
+    @Test
+    public void reservedNameError() {
+        assertNull(selector.getSlingTypeResolver("sling/shouldFail"));
+    }
+
+    @Test
+    public void sameNameTypeResolver() {
+        final SlingTypeResolver<Object> str = selector.getSlingTypeResolver("sling/duplicate");
+        assertNotNull(str);
+        assertEquals(DummyTypeResolver.class, str.getClass());
+        assertNull(str.getType(mock(SlingTypeResolverEnvironment.class)));
+    }
+}
diff --git a/src/test/java/org/apache/sling/graphql/core/mocks/DummyTypeResolver.java b/src/test/java/org/apache/sling/graphql/core/mocks/DummyTypeResolver.java
new file mode 100644
index 0000000..ad44668
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/core/mocks/DummyTypeResolver.java
@@ -0,0 +1,32 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.core.mocks;
+
+import org.apache.sling.graphql.api.SlingTypeResolver;
+import org.apache.sling.graphql.api.SlingTypeResolverEnvironment;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class DummyTypeResolver implements SlingTypeResolver<Object> {
+
+    @Override
+    public @Nullable Object getType(@NotNull SlingTypeResolverEnvironment e) {
+        return null;
+    }
+}
diff --git a/src/test/java/org/apache/sling/graphql/core/mocks/TestUtil.java b/src/test/java/org/apache/sling/graphql/core/mocks/TestUtil.java
index a68d3cb..5a2b415 100644
--- a/src/test/java/org/apache/sling/graphql/core/mocks/TestUtil.java
+++ b/src/test/java/org/apache/sling/graphql/core/mocks/TestUtil.java
@@ -26,6 +26,7 @@ import java.util.Hashtable;
 
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingScalarConverter;
+import org.apache.sling.graphql.api.SlingTypeResolver;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
 import org.osgi.framework.ServiceRegistration;
@@ -44,6 +45,18 @@ public class TestUtil {
         return bc.registerService(SlingDataFetcher.class, f, props);
     }
 
+    public static ServiceRegistration<?> registerSlingTypeResolver(BundleContext bc, String name, SlingTypeResolver<?> tr) {
+        return registerSlingTypeResolver(bc, name, 0, tr);
+    }
+
+    public static ServiceRegistration<?> registerSlingTypeResolver(BundleContext bc, String name, int serviceRanking,
+                                                                   SlingTypeResolver<?> tr) {
+        final Dictionary<String, Object> props = new Hashtable<>();
+        props.put(SlingTypeResolver.NAME_SERVICE_PROPERTY, name);
+        props.put(Constants.SERVICE_RANKING, serviceRanking);
+        return bc.registerService(SlingTypeResolver.class, tr, props);
+    }
+
     public static ServiceRegistration<?> registerSlingScalarConverter(BundleContext bc, String name, SlingScalarConverter<?,?> c) {
         final Dictionary<String, Object> props = new Hashtable<>();
         props.put(SlingScalarConverter.NAME_SERVICE_PROPERTY, name);
diff --git a/src/test/java/org/apache/sling/graphql/core/mocks/TypeSlingResourceDTO.java b/src/test/java/org/apache/sling/graphql/core/mocks/TypeSlingResourceDTO.java
new file mode 100644
index 0000000..fa1339f
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/core/mocks/TypeSlingResourceDTO.java
@@ -0,0 +1,37 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.core.mocks;
+
+public class TypeSlingResourceDTO {
+    private final String path;
+    private final String resourceType;
+
+    public TypeSlingResourceDTO(String path, String resourceType) {
+        this.path = path;
+        this.resourceType = resourceType;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public String getResourceType() {
+        return resourceType;
+    }
+}
diff --git a/src/test/java/org/apache/sling/graphql/core/mocks/TypeTestDTO.java b/src/test/java/org/apache/sling/graphql/core/mocks/TypeTestDTO.java
new file mode 100644
index 0000000..2e087ae
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/core/mocks/TypeTestDTO.java
@@ -0,0 +1,50 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.core.mocks;
+
+public class TypeTestDTO {
+    private final boolean test;
+    private final boolean boolValue;
+    private final String resourcePath;
+    private final String testingArgument;
+
+    public TypeTestDTO(boolean test, boolean boolValue, String resourcePath, String testingArgument) {
+        this.test = test;
+        this.boolValue = boolValue;
+        this.resourcePath = resourcePath;
+        this.testingArgument = testingArgument;
+    }
+
+    public boolean isTest() {
+        return test;
+    }
+
+    public boolean isBoolValue() {
+        return boolValue;
+    }
+
+    public String getResourcePath() {
+        return resourcePath;
+    }
+
+    public String getTestingArgument() {
+        return testingArgument;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/IntrospectionQueryTest.java b/src/test/java/org/apache/sling/graphql/core/mocks/UnionTypeResolver.java
similarity index 50%
copy from src/test/java/org/apache/sling/graphql/core/engine/IntrospectionQueryTest.java
copy to src/test/java/org/apache/sling/graphql/core/mocks/UnionTypeResolver.java
index 4c4d476..61ccc0c 100644
--- a/src/test/java/org/apache/sling/graphql/core/engine/IntrospectionQueryTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/mocks/UnionTypeResolver.java
@@ -16,25 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.sling.graphql.core.engine;
 
-import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
+package org.apache.sling.graphql.core.mocks;
 
-import static org.junit.Assert.assertThat;
-import static org.hamcrest.Matchers.contains;
+import org.apache.sling.graphql.api.SlingTypeResolver;
+import org.apache.sling.graphql.api.SlingTypeResolverEnvironment;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
-import org.junit.Test;
+public class UnionTypeResolver implements SlingTypeResolver<Object> {
 
-public class IntrospectionQueryTest extends ResourceQueryTestBase {
-    
-    @Test
-    public void schemaIntrospectionTest() throws Exception {
-        final String json = queryJSON("{ __schema { types { name } directives { description }}}");
-        assertThat(json, hasJsonPath("$.data.__schema"));
-        assertThat(json, hasJsonPath("$.data.__schema.types"));
-        assertThat(json, hasJsonPath("$.data.__schema.types..name"));
-        assertThat(json, hasJsonPath("$.data.__schema.directives"));
-        assertThat(json, hasJsonPath("$.data.__schema.directives..description"));
+    @Nullable
+    @Override
+    public Object getType(@NotNull SlingTypeResolverEnvironment e) {
+        Object resultItem = e.getObject();
+        if (resultItem instanceof TypeTestDTO) {
+            return e.getObjectType("Test");
+        }
+        if (resultItem instanceof TypeSlingResourceDTO) {
+            return e.getObjectType("SlingResource");
+        }
+        return null;
     }
-
 }
diff --git a/src/test/java/org/apache/sling/graphql/core/schema/SchemaDescriptionsTest.java b/src/test/java/org/apache/sling/graphql/core/schema/SchemaDescriptionsTest.java
index a4147dd..2c093ba 100644
--- a/src/test/java/org/apache/sling/graphql/core/schema/SchemaDescriptionsTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/schema/SchemaDescriptionsTest.java
@@ -30,7 +30,10 @@ import org.apache.sling.graphql.api.SchemaProvider;
 import org.apache.sling.graphql.api.engine.QueryExecutor;
 import org.apache.sling.graphql.core.engine.DefaultQueryExecutor;
 import org.apache.sling.graphql.core.engine.SlingDataFetcherSelector;
+import org.apache.sling.graphql.core.engine.SlingTypeResolverSelector;
 import org.apache.sling.graphql.core.mocks.MockSchemaProvider;
+import org.apache.sling.graphql.core.mocks.TestUtil;
+import org.apache.sling.graphql.core.mocks.UnionTypeResolver;
 import org.apache.sling.graphql.core.scalars.SlingScalarsProvider;
 import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
 import org.hamcrest.CustomMatcher;
@@ -103,6 +106,8 @@ public class SchemaDescriptionsTest {
         final ServletResolver servletResolver = Mockito.mock(ServletResolver.class);
         context.bundleContext().registerService(ServletResolver.class, servletResolver, null);
         context.registerInjectActivateService(new SlingDataFetcherSelector());
+        context.registerInjectActivateService(new SlingTypeResolverSelector());
+        TestUtil.registerSlingTypeResolver(context.bundleContext(), "union/resolver", new UnionTypeResolver());
         context.registerInjectActivateService(new SlingScalarsProvider());
         context.registerService(SchemaProvider.class, new MockSchemaProvider("test-schema"));
         context.registerInjectActivateService(new RankedSchemaProviders());
diff --git a/src/test/resources/test-schema.txt b/src/test/resources/failing-fetcher-schema.txt
similarity index 56%
copy from src/test/resources/test-schema.txt
copy to src/test/resources/failing-fetcher-schema.txt
index 230bc24..a559d0f 100644
--- a/src/test/resources/test-schema.txt
+++ b/src/test/resources/failing-fetcher-schema.txt
@@ -22,38 +22,11 @@ directive @fetcher(
     source : String = ""
 ) on FIELD_DEFINITION
 
-# GraphQL Schema used for our tests
+# Schema used to test invalid fetcher names
 type Query {
     currentResource : SlingResource @fetcher(name:"echoNS/echo")
-
-    # Test some static values
-    staticContent: Test @fetcher(name:"test/static")
 }
 
-# This should be omitted from the SlingResource type description
-#
-# SlingResource, for our tests
 type SlingResource { 
-    path: String
-    resourceType: String
-
-    pathMD5: String @fetcher(name:"sling/digest" options:"md5" source:"path")
-
-    # SHA256 digest of the path
-    pathSHA256: String @fetcher(name:"sling/digest" options:"sha-256" source:"path")
-
-    # MD5 digest of the resource type
-    resourceTypeMD5: String @fetcher(name:"sling/digest" options:"md5" source:"resourceType")
-
-    nullValue: String @fetcher(name:"echoNS/echo" options:"null")
-
-    # Failure message
-    failure: String @fetcher(name:"failure/fail")
+    missingSlash: String @fetcher(name:"missingSlash")
 }
-
-type Test { 
-    test: Boolean
-    boolValue: Boolean
-    resourcePath: String
-    testingArgument: String
-}
\ No newline at end of file
diff --git a/src/test/resources/test-schema.txt b/src/test/resources/failing-type-resolver-schema.txt
similarity index 59%
copy from src/test/resources/test-schema.txt
copy to src/test/resources/failing-type-resolver-schema.txt
index 230bc24..7451b3a 100644
--- a/src/test/resources/test-schema.txt
+++ b/src/test/resources/failing-type-resolver-schema.txt
@@ -16,11 +16,11 @@
 # * under the License.
 
 # This directive maps fields to our Sling data fetchers
-directive @fetcher(
+directive @resolver(
     name : String,
     options : String = "",
     source : String = ""
-) on FIELD_DEFINITION
+) on UNION
 
 # GraphQL Schema used for our tests
 type Query {
@@ -28,32 +28,9 @@ type Query {
 
     # Test some static values
     staticContent: Test @fetcher(name:"test/static")
-}
-
-# This should be omitted from the SlingResource type description
-#
-# SlingResource, for our tests
-type SlingResource { 
-    path: String
-    resourceType: String
-
-    pathMD5: String @fetcher(name:"sling/digest" options:"md5" source:"path")
-
-    # SHA256 digest of the path
-    pathSHA256: String @fetcher(name:"sling/digest" options:"sha-256" source:"path")
-
-    # MD5 digest of the resource type
-    resourceTypeMD5: String @fetcher(name:"sling/digest" options:"md5" source:"resourceType")
-
-    nullValue: String @fetcher(name:"echoNS/echo" options:"null")
 
-    # Failure message
-    failure: String @fetcher(name:"failure/fail")
+    # Test union as return type
+    unionFetcher: Test2 @fetcher(name:"union/fetcher")
 }
 
-type Test { 
-    test: Boolean
-    boolValue: Boolean
-    resourcePath: String
-    testingArgument: String
-}
\ No newline at end of file
+union AllTypes @resolver(name:"missingSlash" source:"AllTypes") = SlingResource | Test
diff --git a/src/test/resources/test-schema.txt b/src/test/resources/test-schema.txt
index 230bc24..e0853a8 100644
--- a/src/test/resources/test-schema.txt
+++ b/src/test/resources/test-schema.txt
@@ -22,14 +22,26 @@ directive @fetcher(
     source : String = ""
 ) on FIELD_DEFINITION
 
+# This directive to maps types
+directive @resolver(
+    name : String,
+    options : String = "",
+    source : String = ""
+) on UNION
+
 # GraphQL Schema used for our tests
 type Query {
     currentResource : SlingResource @fetcher(name:"echoNS/echo")
 
     # Test some static values
     staticContent: Test @fetcher(name:"test/static")
+
+    # Test union as return type
+    unionFetcher: Test2 @fetcher(name:"union/fetcher")
 }
 
+union AllTypes @resolver(name:"union/resolver" source:"AllTypes") = SlingResource | Test
+
 # This should be omitted from the SlingResource type description
 #
 # SlingResource, for our tests
@@ -56,4 +68,9 @@ type Test {
     boolValue: Boolean
     resourcePath: String
     testingArgument: String
-}
\ No newline at end of file
+}
+
+type Test2 {
+    items: [AllTypes]
+}
+