docs: public Object expand(Context activeCtx, String activeProperty, Object element)
              throws JsonLdError {
          final boolean frameExpansion = this.opts.getFrameExpansion();
          // 1)
          if (element == null) {
              return null;
          }
  
          // 3)
          if (element instanceof List) {
              // 3.1)
              final List<Object> result = new ArrayList<Object>();
              // 3.2)
              for (final Object item : (List<Object>) element) {
                  // 3.2.1)
                  final Object v = expand(activeCtx, activeProperty, item);
                  // 3.2.2)
                  if ((JsonLdConsts.LIST.equals(activeProperty)
                          || JsonLdConsts.LIST.equals(activeCtx.getContainer(activeProperty)))
                          && (v instanceof List || (v instanceof Map
                                  && ((Map<String, Object>) v).containsKey(JsonLdConsts.LIST)))) {
                      throw new JsonLdError(Error.LIST_OF_LISTS, "lists of lists are not permitted.");
                  }
                  // 3.2.3)
                  else if (v != null) {
                      if (v instanceof List) {
                          result.addAll((Collection<? extends Object>) v);
                      } else {
                          result.add(v);
                      }
                  }
              }
              // 3.3)
              return result;
          }
          // 4)
          else if (element instanceof Map) {
              // access helper
              final Map<String, Object> elem = (Map<String, Object>) element;
              // 5)
              if (elem.containsKey(JsonLdConsts.CONTEXT)) {
                  activeCtx = activeCtx.parse(elem.get(JsonLdConsts.CONTEXT));
              }
              // 6)
              Map<String, Object> result = newMap();
              // 7)
              final List<String> keys = new ArrayList<String>(elem.keySet());
              Collections.sort(keys);
              for (final String key : keys) {
                  final Object value = elem.get(key);
                  // 7.1)
                  if (key.equals(JsonLdConsts.CONTEXT)) {
                      continue;
                  }
                  // 7.2)
                  final String expandedProperty = activeCtx.expandIri(key, false, true, null, null);
                  Object expandedValue = null;
                  // 7.3)
                  if (expandedProperty == null
                          || (!expandedProperty.contains(":") && !isKeyword(expandedProperty))) {
                      continue;
                  }
                  // 7.4)
                  if (isKeyword(expandedProperty)) {
                      // 7.4.1)
                      if (JsonLdConsts.REVERSE.equals(activeProperty)) {
                          throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_MAP,
                                  "a keyword cannot be used as a @reverse propery");
                      }
                      // 7.4.2)
                      if (result.containsKey(expandedProperty)) {
                          throw new JsonLdError(Error.COLLIDING_KEYWORDS,
                                  expandedProperty + " already exists in result");
                      }
                      // 7.4.3)
                      if (JsonLdConsts.ID.equals(expandedProperty)) {
                          if (value instanceof String) {
                              expandedValue = activeCtx.expandIri((String) value, true, false, null,
                                      null);
                          } else if (frameExpansion) {
                              if (value instanceof Map) {
                                  if (((Map<String, Object>) value).size() != 0) {
                                      throw new JsonLdError(Error.INVALID_ID_VALUE,
                                              "@id value must be a an empty object for framing");
                                  }
                                  expandedValue = value;
                              } else if (value instanceof List) {
                                  expandedValue = new ArrayList<String>();
                                  for (final Object v : (List<Object>) value) {
                                      if (!(v instanceof String)) {
                                          throw new JsonLdError(Error.INVALID_ID_VALUE,
                                                  "@id value must be a string, an array of strings or an empty dictionary");
                                      }
                                      ((List<String>) expandedValue).add(activeCtx
                                              .expandIri((String) v, true, true, null, null));
                                  }
                              } else {
                                  throw new JsonLdError(Error.INVALID_ID_VALUE,
                                          "value of @id must be a string, an array of strings or an empty dictionary");
                              }
                          } else {
                              throw new JsonLdError(Error.INVALID_ID_VALUE,
                                      "value of @id must be a string");
                          }
                      }
                      // 7.4.4)
                      else if (JsonLdConsts.TYPE.equals(expandedProperty)) {
                          if (value instanceof List) {
                              expandedValue = new ArrayList<String>();
                              for (final Object v : (List) value) {
                                  if (!(v instanceof String)) {
                                      throw new JsonLdError(Error.INVALID_TYPE_VALUE,
                                              "@type value must be a string or array of strings");
                                  }
                                  ((List<String>) expandedValue).add(
                                          activeCtx.expandIri((String) v, true, true, null, null));
                              }
                          } else if (value instanceof String) {
                              expandedValue = activeCtx.expandIri((String) value, true, true, null,
                                      null);
                          }
                          // TODO: SPEC: no mention of empty map check
                          else if (frameExpansion && value instanceof Map) {
                              if (!((Map<String, Object>) value).isEmpty()) {
                                  throw new JsonLdError(Error.INVALID_TYPE_VALUE,
                                          "@type value must be a an empty object for framing");
                              }
                              expandedValue = value;
                          } else {
                              throw new JsonLdError(Error.INVALID_TYPE_VALUE,
                                      "@type value must be a string or array of strings");
                          }
                      }
                      // 7.4.5)
                      else if (JsonLdConsts.GRAPH.equals(expandedProperty)) {
                          expandedValue = expand(activeCtx, JsonLdConsts.GRAPH, value);
                      }
                      // 7.4.6)
                      else if (JsonLdConsts.VALUE.equals(expandedProperty)) {
                          if (value != null && (value instanceof Map || value instanceof List)) {
                              throw new JsonLdError(Error.INVALID_VALUE_OBJECT_VALUE,
                                      "value of " + expandedProperty + " must be a scalar or null");
                          }
                          expandedValue = value;
                          if (expandedValue == null) {
                              result.put(JsonLdConsts.VALUE, null);
                              continue;
                          }
                      }
                      // 7.4.7)
                      else if (JsonLdConsts.LANGUAGE.equals(expandedProperty)) {
                          if (!(value instanceof String)) {
                              throw new JsonLdError(Error.INVALID_LANGUAGE_TAGGED_STRING,
                                      "Value of " + expandedProperty + " must be a string");
                          }
                          expandedValue = ((String) value).toLowerCase();
                      }
                      // 7.4.8)
                      else if (JsonLdConsts.INDEX.equals(expandedProperty)) {
                          if (!(value instanceof String)) {
                              throw new JsonLdError(Error.INVALID_INDEX_VALUE,
                                      "Value of " + expandedProperty + " must be a string");
                          }
                          expandedValue = value;
                      }
                      // 7.4.9)
                      else if (JsonLdConsts.LIST.equals(expandedProperty)) {
                          // 7.4.9.1)
                          if (activeProperty == null || JsonLdConsts.GRAPH.equals(activeProperty)) {
                              continue;
                          }
                          // 7.4.9.2)
                          expandedValue = expand(activeCtx, activeProperty, value);
  
                          // NOTE: step not in the spec yet
                          if (!(expandedValue instanceof List)) {
                              final List<Object> tmp = new ArrayList<Object>();
                              tmp.add(expandedValue);
                              expandedValue = tmp;
                          }
  
                          // 7.4.9.3)
                          for (final Object o : (List<Object>) expandedValue) {
                              if (o instanceof Map
                                      && ((Map<String, Object>) o).containsKey(JsonLdConsts.LIST)) {
                                  throw new JsonLdError(Error.LIST_OF_LISTS,
                                          "A list may not contain another list");
                              }
                          }
                      }
                      // 7.4.10)
                      else if (JsonLdConsts.SET.equals(expandedProperty)) {
                          expandedValue = expand(activeCtx, activeProperty, value);
                      }
                      // 7.4.11)
                      else if (JsonLdConsts.REVERSE.equals(expandedProperty)) {
                          if (!(value instanceof Map)) {
                              throw new JsonLdError(Error.INVALID_REVERSE_VALUE,
                                      "@reverse value must be an object");
                          }
                          // 7.4.11.1)
                          expandedValue = expand(activeCtx, JsonLdConsts.REVERSE, value);
                          // NOTE: algorithm assumes the result is a map
                          // 7.4.11.2)
                          if (((Map<String, Object>) expandedValue)
                                  .containsKey(JsonLdConsts.REVERSE)) {
                              final Map<String, Object> reverse = (Map<String, Object>) ((Map<String, Object>) expandedValue)
                                      .get(JsonLdConsts.REVERSE);
                              for (final String property : reverse.keySet()) {
                                  final Object item = reverse.get(property);
                                  // 7.4.11.2.1)
                                  if (!result.containsKey(property)) {
                                      result.put(property, new ArrayList<Object>());
                                  }
                                  // 7.4.11.2.2)
                                  if (item instanceof List) {
                                      ((List<Object>) result.get(property))
                                              .addAll((List<Object>) item);
                                  } else {
                                      ((List<Object>) result.get(property)).add(item);
                                  }
                              }
                          }
                          // 7.4.11.3)
                          if (((Map<String, Object>) expandedValue)
                                  .size() > (((Map<String, Object>) expandedValue)
                                          .containsKey(JsonLdConsts.REVERSE) ? 1 : 0)) {
                              // 7.4.11.3.1)
                              if (!result.containsKey(JsonLdConsts.REVERSE)) {
                                  result.put(JsonLdConsts.REVERSE, newMap());
                              }
                              // 7.4.11.3.2)
                              final Map<String, Object> reverseMap = (Map<String, Object>) result
                                      .get(JsonLdConsts.REVERSE);
                              // 7.4.11.3.3)
                              for (final String property : ((Map<String, Object>) expandedValue)
                                      .keySet()) {
                                  if (JsonLdConsts.REVERSE.equals(property)) {
                                      continue;
                                  }
                                  // 7.4.11.3.3.1)
                                  final List<Object> items = (List<Object>) ((Map<String, Object>) expandedValue)
                                          .get(property);
                                  for (final Object item : items) {
                                      // 7.4.11.3.3.1.1)
                                      if (item instanceof Map && (((Map<String, Object>) item)
                                              .containsKey(JsonLdConsts.VALUE)
                                              || ((Map<String, Object>) item)
                                                      .containsKey(JsonLdConsts.LIST))) {
                                          throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_VALUE);
                                      }
                                      // 7.4.11.3.3.1.2)
                                      if (!reverseMap.containsKey(property)) {
                                          reverseMap.put(property, new ArrayList<Object>());
                                      }
                                      // 7.4.11.3.3.1.3)
                                      ((List<Object>) reverseMap.get(property)).add(item);
                                  }
                              }
                          }
                          // 7.4.11.4)
                          continue;
                      }
                      // TODO: SPEC no mention of @explicit etc in spec
                      else if (frameExpansion && (JsonLdConsts.EXPLICIT.equals(expandedProperty)
                              || JsonLdConsts.DEFAULT.equals(expandedProperty)
                              || JsonLdConsts.EMBED.equals(expandedProperty)
                              || JsonLdConsts.REQUIRE_ALL.equals(expandedProperty)
                              || JsonLdConsts.EMBED_CHILDREN.equals(expandedProperty)
                              || JsonLdConsts.OMIT_DEFAULT.equals(expandedProperty))) {
                          expandedValue = expand(activeCtx, expandedProperty, value);
                      }
                      // 7.4.12)
                      if (expandedValue != null) {
                          result.put(expandedProperty, expandedValue);
                      }
                      // 7.4.13)
                      continue;
                  }
                  // 7.5
                  else if (JsonLdConsts.LANGUAGE.equals(activeCtx.getContainer(key))
                          && value instanceof Map) {
                      // 7.5.1)
                      expandedValue = new ArrayList<Object>();
                      // 7.5.2)
                      for (final String language : ((Map<String, Object>) value).keySet()) {
                          Object languageValue = ((Map<String, Object>) value).get(language);
                          // 7.5.2.1)
                          if (!(languageValue instanceof List)) {
                              final Object tmp = languageValue;
                              languageValue = new ArrayList<Object>();
                              ((List<Object>) languageValue).add(tmp);
                          }
                          // 7.5.2.2)
                          for (final Object item : (List<Object>) languageValue) {
                              // 7.5.2.2.1)
                              if (!(item instanceof String)) {
                                  throw new JsonLdError(Error.INVALID_LANGUAGE_MAP_VALUE,
                                          "Expected " + item.toString() + " to be a string");
                              }
                              // 7.5.2.2.2)
                              final Map<String, Object> tmp = newMap();
                              tmp.put(JsonLdConsts.VALUE, item);
                              tmp.put(JsonLdConsts.LANGUAGE, language.toLowerCase());
                              ((List<Object>) expandedValue).add(tmp);
                          }
                      }
                  }
                  // 7.6)
                  else if (JsonLdConsts.INDEX.equals(activeCtx.getContainer(key))
                          && value instanceof Map) {
                      // 7.6.1)
                      expandedValue = new ArrayList<Object>();
                      // 7.6.2)
                      final List<String> indexKeys = new ArrayList<String>(
                              ((Map<String, Object>) value).keySet());
                      Collections.sort(indexKeys);
                      for (final String index : indexKeys) {
                          Object indexValue = ((Map<String, Object>) value).get(index);
                          // 7.6.2.1)
                          if (!(indexValue instanceof List)) {
                              final Object tmp = indexValue;
                              indexValue = new ArrayList<Object>();
                              ((List<Object>) indexValue).add(tmp);
                          }
                          // 7.6.2.2)
                          indexValue = expand(activeCtx, key, indexValue);
                          // 7.6.2.3)
                          for (final Map<String, Object> item : (List<Map<String, Object>>) indexValue) {
                              // 7.6.2.3.1)
                              if (!item.containsKey(JsonLdConsts.INDEX)) {
                                  item.put(JsonLdConsts.INDEX, index);
                              }
                              // 7.6.2.3.2)
                              ((List<Object>) expandedValue).add(item);
                          }
                      }
                  }
                  // 7.7)
                  else {
                      expandedValue = expand(activeCtx, key, value);
                  }
                  // 7.8)
                  if (expandedValue == null) {
                      continue;
                  }
                  // 7.9)
                  if (JsonLdConsts.LIST.equals(activeCtx.getContainer(key))) {
                      if (!(expandedValue instanceof Map) || !((Map<String, Object>) expandedValue)
                              .containsKey(JsonLdConsts.LIST)) {
                          Object tmp = expandedValue;
                          if (!(tmp instanceof List)) {
                              tmp = new ArrayList<Object>();
                              ((List<Object>) tmp).add(expandedValue);
                          }
                          expandedValue = newMap();
                          ((Map<String, Object>) expandedValue).put(JsonLdConsts.LIST, tmp);
                      }
                  }
                  // 7.10)
                  if (activeCtx.isReverseProperty(key)) {
                      // 7.10.1)
                      if (!result.containsKey(JsonLdConsts.REVERSE)) {
                          result.put(JsonLdConsts.REVERSE, newMap());
                      }
                      // 7.10.2)
                      final Map<String, Object> reverseMap = (Map<String, Object>) result
                              .get(JsonLdConsts.REVERSE);
                      // 7.10.3)
                      if (!(expandedValue instanceof List)) {
                          final Object tmp = expandedValue;
                          expandedValue = new ArrayList<Object>();
                          ((List<Object>) expandedValue).add(tmp);
                      }
                      // 7.10.4)
                      for (final Object item : (List<Object>) expandedValue) {
                          // 7.10.4.1)
                          if (item instanceof Map && (((Map<String, Object>) item)
                                  .containsKey(JsonLdConsts.VALUE)
                                  || ((Map<String, Object>) item).containsKey(JsonLdConsts.LIST))) {
                              throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_VALUE);
                          }
                          // 7.10.4.2)
                          if (!reverseMap.containsKey(expandedProperty)) {
                              reverseMap.put(expandedProperty, new ArrayList<Object>());
                          }
                          // 7.10.4.3)
                          if (item instanceof List) {
                              ((List<Object>) reverseMap.get(expandedProperty))
                                      .addAll((List<Object>) item);
                          } else {
                              ((List<Object>) reverseMap.get(expandedProperty)).add(item);
                          }
                      }
                  }
                  // 7.11)
                  else {
                      // 7.11.1)
                      if (!result.containsKey(expandedProperty)) {
                          result.put(expandedProperty, new ArrayList<Object>());
                      }
                      // 7.11.2)
                      if (expandedValue instanceof List) {
                          ((List<Object>) result.get(expandedProperty))
                                  .addAll((List<Object>) expandedValue);
                      } else {
                          ((List<Object>) result.get(expandedProperty)).add(expandedValue);
                      }
                  }
              }
              // 8)
              if (result.containsKey(JsonLdConsts.VALUE)) {
                  // 8.1)
                  // TODO: is this method faster than just using containsKey for
                  // each?
                  final Set<String> keySet = new HashSet<>(result.keySet());
                  keySet.remove(JsonLdConsts.VALUE);
                  keySet.remove(JsonLdConsts.INDEX);
                  final boolean langremoved = keySet.remove(JsonLdConsts.LANGUAGE);
                  final boolean typeremoved = keySet.remove(JsonLdConsts.TYPE);
                  if ((langremoved && typeremoved) || !keySet.isEmpty()) {
                      throw new JsonLdError(Error.INVALID_VALUE_OBJECT,
                              "value object has unknown keys");
                  }
                  // 8.2)
                  final Object rval = result.get(JsonLdConsts.VALUE);
                  if (rval == null) {
                      // nothing else is possible with result if we set it to
                      // null, so simply return it
                      return null;
                  }
                  // 8.3)
                  if (!(rval instanceof String) && result.containsKey(JsonLdConsts.LANGUAGE)) {
                      throw new JsonLdError(Error.INVALID_LANGUAGE_TAGGED_VALUE,
                              "when @language is used, @value must be a string");
                  }
                  // 8.4)
                  else if (result.containsKey(JsonLdConsts.TYPE)) {
                      // TODO: is this enough for "is an IRI"
                      if (!(result.get(JsonLdConsts.TYPE) instanceof String)
                              || ((String) result.get(JsonLdConsts.TYPE)).startsWith("_:")
                              || !((String) result.get(JsonLdConsts.TYPE)).contains(":")) {
                          throw new JsonLdError(Error.INVALID_TYPED_VALUE,
                                  "value of @type must be an IRI");
                      }
                  }
              }
              // 9)
              else if (result.containsKey(JsonLdConsts.TYPE)) {
                  final Object rtype = result.get(JsonLdConsts.TYPE);
                  if (!(rtype instanceof List)) {
                      final List<Object> tmp = new ArrayList<Object>();
                      tmp.add(rtype);
                      result.put(JsonLdConsts.TYPE, tmp);
                  }
              }
              // 10)
              else if (result.containsKey(JsonLdConsts.SET)
                      || result.containsKey(JsonLdConsts.LIST)) {
                  // 10.1)
                  if (result.size() > (result.containsKey(JsonLdConsts.INDEX) ? 2 : 1)) {
                      throw new JsonLdError(Error.INVALID_SET_OR_LIST_OBJECT,
                              "@set or @list may only contain @index");
                  }
                  // 10.2)
                  if (result.containsKey(JsonLdConsts.SET)) {
                      // result becomes an array here, thus the remaining checks
                      // will never be true from here on
                      // so simply return the value rather than have to make
                      // result an object and cast it with every
                      // other use in the function.
                      return result.get(JsonLdConsts.SET);
                  }
              }
              // 11)
              if (result.containsKey(JsonLdConsts.LANGUAGE) && result.size() == 1) {
                  result = null;
              }
              // 12)
              if (activeProperty == null || JsonLdConsts.GRAPH.equals(activeProperty)) {
                  // 12.1)
                  if (result != null && (result.size() == 0 || result.containsKey(JsonLdConsts.VALUE)
                          || result.containsKey(JsonLdConsts.LIST))) {
                      result = null;
                  }
                  // 12.2)
                  else if (result != null && !frameExpansion && result.containsKey(JsonLdConsts.ID)
                          && result.size() == 1) {
                      result = null;
                  }
              }
              // 13)
              return result;
          }
          // 2) If element is a scalar
          else {
              // 2.1)
              if (activeProperty == null || JsonLdConsts.GRAPH.equals(activeProperty)) {
                  return null;
              }
              return activeCtx.expandValue(activeProperty, element);
          }
      }