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/09 13:57:54 UTC

[sling-org-apache-sling-graphql-core] 01/01: 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 issue/SLING-9796
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git

commit 9e3de633e58df6d98f46a155f81db0ae37957e04
Author: Radu Cotescu <ra...@apache.org>
AuthorDate: Fri Oct 9 15:52:43 2020 +0200

    SLING-9796 - Add support for using Unions
    
    Co-authored-by: Radu Cotescu <ra...@apache.org>
    Co-authored-by: Adrian Kozma <ko...@adobe.com>
---
 README.md                                          |  30 ++++-
 .../sling/graphql/api/SlingTypeResolver.java       |  41 ++++++
 .../graphql/api/SlingTypeResolverEnvironment.java  |  65 +++++++++
 .../graphql/core/engine/DefaultQueryExecutor.java  |  67 ++++++++--
 .../core/engine/SlingTypeResolverSelector.java     | 145 +++++++++++++++++++++
 .../core/engine/SlingTypeResolverWrapper.java      |  54 ++++++++
 .../engine/TypeResolverEnvironmentWrapper.java     |  73 +++++++++++
 .../example/resolvers/DoNothingTypeResolver.java   |  36 +++++
 .../core/engine/DefaultQueryExecutorTest.java      |  43 +++++-
 .../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} |  33 +----
 ...schema.txt => failing-type-resolver-schema.txt} |  33 +----
 src/test/resources/test-schema.txt                 |  20 ++-
 22 files changed, 894 insertions(+), 91 deletions(-)

diff --git a/README.md b/README.md
index ef2f229..32bb24d 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,7 @@ 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.
@@ -130,6 +130,34 @@ The data fetcher provider then looks for a script that handles the `graphql/fetc
 
 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 2b87dab..93d472d 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
@@ -25,11 +25,14 @@ import javax.json.Json;
 import javax.json.JsonObject;
 import javax.script.ScriptException;
 
+import graphql.language.UnionTypeDefinition;
+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.SlingTypeResolver;
 import org.apache.sling.graphql.core.scalars.SlingScalarsProvider;
 import org.apache.sling.graphql.core.schema.RankedSchemaProviders;
 import org.jetbrains.annotations.NotNull;
@@ -69,6 +72,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;
 
@@ -76,6 +84,9 @@ public class DefaultQueryExecutor implements QueryExecutor {
     private SlingDataFetcherSelector dataFetcherSelector;
 
     @Reference
+    private SlingTypeResolverSelector typeResolverSelector;
+
+    @Reference
     private SlingScalarsProvider scalarsProvider;
 
     @Override
@@ -84,8 +95,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)
@@ -104,7 +114,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()
@@ -131,24 +141,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);
                         }
@@ -162,6 +170,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();
     }
 
@@ -181,7 +203,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);
@@ -189,7 +219,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);
             }
@@ -197,6 +227,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..3cd7524
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/engine/SlingTypeResolverSelector.java
@@ -0,0 +1,145 @@
+/*
+ * 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.io.IOException;
+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.SlingDataFetcher;
+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 SlingDataFetcher {}: fetcher names starting with '{}' are reserved for Apache Sling Java packages",
+                            className, RESERVED_NAME_PREFIX);
+                    return false;
+                }
+            }
+        } else {
+            LOGGER.error("Invalid {} implementation: fetcher {} is missing the mandatory value for its {} service property.",
+                    SlingDataFetcher.class.getName(), slingTypeResolver.getClass().getName(), SlingDataFetcher.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 41a0e4b..7e567e1 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,18 +18,24 @@
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 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.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;
@@ -44,10 +50,19 @@ import static org.junit.Assert.assertThat;
 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));
@@ -116,14 +131,30 @@ 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();
         }
@@ -138,4 +169,12 @@ public class DefaultQueryExecutorTest extends ResourceQueryTestBase {
         assertThat(json, hasJsonPath("$.data.scriptedFetcher.resourcePath", equalTo(resource.getPath())));
         assertThat(json, hasJsonPath("$.data.scriptedFetcher.testingArgument", equalTo("1, 2, 3")));
     }
+
+    @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 c5b3095..f33340f 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
@@ -66,6 +66,7 @@ public abstract class ResourceQueryTestBase {
 
         context.registerInjectActivateService(new ScriptedDataFetcherProvider());
         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 0b247a2..48500cd 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.engine.QueryExecutor;
 import org.apache.sling.graphql.core.engine.DefaultQueryExecutor;
 import org.apache.sling.graphql.core.engine.ScriptedDataFetcherProvider;
 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;
@@ -104,6 +107,8 @@ public class SchemaDescriptionsTest {
         context.bundleContext().registerService(ServletResolver.class, servletResolver, null);
         context.registerInjectActivateService(new ScriptedDataFetcherProvider());
         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 54%
copy from src/test/resources/test-schema.txt
copy to src/test/resources/failing-fetcher-schema.txt
index 0bcaa5d..a559d0f 100644
--- a/src/test/resources/test-schema.txt
+++ b/src/test/resources/failing-fetcher-schema.txt
@@ -22,40 +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")
-
-    scriptedFetcher (testing : String) : Test @fetcher(name:"scripted/example")
 }
 
-# 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 61%
copy from src/test/resources/test-schema.txt
copy to src/test/resources/failing-type-resolver-schema.txt
index 0bcaa5d..2e9694d 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 {
@@ -30,32 +30,9 @@ type Query {
     staticContent: Test @fetcher(name:"test/static")
 
     scriptedFetcher (testing : String) : Test @fetcher(name:"scripted/example")
-}
-
-# 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 0bcaa5d..2ea0466 100644
--- a/src/test/resources/test-schema.txt
+++ b/src/test/resources/test-schema.txt
@@ -22,6 +22,13 @@ 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")
@@ -30,8 +37,13 @@ type Query {
     staticContent: Test @fetcher(name:"test/static")
 
     scriptedFetcher (testing : String) : Test @fetcher(name:"scripted/example")
+
+    # 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
@@ -51,6 +63,7 @@ type SlingResource {
 
     # Failure message
     failure: String @fetcher(name:"failure/fail")
+
 }
 
 type Test { 
@@ -58,4 +71,9 @@ type Test {
     boolValue: Boolean
     resourcePath: String
     testingArgument: String
-}
\ No newline at end of file
+}
+
+type Test2 {
+    items: [AllTypes]
+}
+