docs: @SuppressWarnings({ "unchecked", "rawtypes" })
      private static Object toJSONGraph(final Object obj, final Set<ReferenceEqualityKey<Object>> visitedOnPath,
              final Set<ReferenceEqualityKey<Object>> standardObjectVisited, final ClassFieldCache classFieldCache,
              final Map<ReferenceEqualityKey<Object>, JSONObject> objToJSONVal,
              final boolean onlySerializePublicFields) {
  
          // For null and basic value types, just return value
          if (JSONUtils.isBasicValueType(obj)) {
              return obj;
          }
  
          // Check for cycles
          final ReferenceEqualityKey<Object> objKey = new ReferenceEqualityKey<>(obj);
          if (!visitedOnPath.add(objKey)) {
              // Reached cycle in graph
              if (JSONUtils.isCollectionOrArray(obj)) {
                  // If we reached a collection that has already been visited, then there is a cycle
                  // terminating at this collection. We don't support collection cycles, since collections
                  // do not have object ids.
                  throw new IllegalArgumentException(
                          "Cycles involving collections cannot be serialized, since collections are not "
                                  + "assigned object ids. Reached cycle at: " + obj);
              } else {
                  // Object is its own ancestor -- output object reference instead of object to break cycle
                  return new JSONReference(obj);
              }
          }
  
          Object jsonVal;
          final Class<?> cls = obj.getClass();
  
          if (Map.class.isAssignableFrom(cls)) {
              final Map<Object, Object> map = (Map<Object, Object>) obj;
  
              // Get map keys, and sort them by value if values are Comparable.
              // Assumes all keys have the same type (or at least that if one key is Comparable, they all are).
              final ArrayList<?> keys = new ArrayList<>(map.keySet());
              final int n = keys.size();
              boolean keysComparable = false;
              Object firstNonNullKey = null;
              for (int i = 0; i < n && firstNonNullKey == null; i++) {
                  firstNonNullKey = keys.get(i);
              }
              if (firstNonNullKey != null && Comparable.class.isAssignableFrom(firstNonNullKey.getClass())) {
                  CollectionUtils.sortIfNotEmpty((ArrayList<Comparable>) keys);
                  keysComparable = true;
              }
  
              // Serialize keys into string form
              final String[] convertedKeys = new String[n];
              for (int i = 0; i < n; i++) {
                  final Object key = keys.get(i);
                  if (!JSONUtils.isBasicValueType(key)) {
                      throw new IllegalArgumentException("Map key of type " + key.getClass().getName()
                              + " is not a basic type (String, Integer, etc.), so can't be easily "
                              + "serialized as a JSON associative array key");
                  }
                  convertedKeys[i] = JSONUtils.escapeJSONString(key.toString());
              }
  
              // Sort value strings lexicographically if values are not Comparable
              if (!keysComparable) {
                  Arrays.sort(convertedKeys);
              }
  
              // Convert map values to JSON values
              final Object[] convertedVals = new Object[n];
              for (int i = 0; i < n; i++) {
                  convertedVals[i] = map.get(keys.get(i));
              }
              convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal,
                      onlySerializePublicFields);
  
              // Create new JSON object representing the map
              final List<Entry<String, Object>> convertedKeyValPairs = new ArrayList<>(n);
              for (int i = 0; i < n; i++) {
                  convertedKeyValPairs.add(new SimpleEntry<>(convertedKeys[i], convertedVals[i]));
              }
              jsonVal = new JSONObject(convertedKeyValPairs);
  
          } else if (cls.isArray() || List.class.isAssignableFrom(cls)) {
              // Serialize an array or list
              final boolean isList = List.class.isAssignableFrom(cls);
              final List<?> list = isList ? (List<?>) obj : null;
              final int n = isList ? list.size() : Array.getLength(obj);
  
              // Convert list items to JSON values
              final Object[] convertedVals = new Object[n];
              for (int i = 0; i < n; i++) {
                  convertedVals[i] = isList ? list.get(i) : Array.get(obj, i);
              }
              convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal,
                      onlySerializePublicFields);
  
              // Create new JSON array representing the list
              jsonVal = new JSONArray(Arrays.asList(convertedVals));
  
          } else if (Collection.class.isAssignableFrom(cls)) {
              final Collection<?> collection = (Collection<?>) obj;
  
              // If collection is a set, need to sort values into some sort of consistent order 
              final List<Object> convertedValsList = new ArrayList<>(collection);
              if (Set.class.isAssignableFrom(cls)) {
                  CollectionUtils.sortIfNotEmpty(convertedValsList, SET_COMPARATOR);
              }
  
              // Convert items to JSON values
              final Object[] convertedVals = convertedValsList.toArray();
              convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal,
                      onlySerializePublicFields);
  
              // Create new JSON array representing the collection
              jsonVal = new JSONArray(Arrays.asList(convertedVals));
  
          } else {
              // A standard object -- serialize fields as a JSON associative array.
              try {
                  // Cache class fields to include in serialization (typeResolutions can be null,
                  // since it's not necessary to resolve type parameters during serialization)
                  final ClassFields resolvedFields = classFieldCache.get(cls);
                  final List<FieldTypeInfo> fieldOrder = resolvedFields.fieldOrder;
                  final int n = fieldOrder.size();
  
                  // Convert field values to JSON values
                  final String[] fieldNames = new String[n];
                  final Object[] convertedVals = new Object[n];
                  for (int i = 0; i < n; i++) {
                      final FieldTypeInfo fieldInfo = fieldOrder.get(i);
                      final Field field = fieldInfo.field;
                      fieldNames[i] = field.getName();
                      convertedVals[i] = JSONUtils.getFieldValue(obj, field);
                  }
                  convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal,
                          onlySerializePublicFields);
  
                  // Create new JSON object representing the standard object
                  final List<Entry<String, Object>> convertedKeyValPairs = new ArrayList<>(n);
                  for (int i = 0; i < n; i++) {
                      convertedKeyValPairs.add(new SimpleEntry(fieldNames[i], convertedVals[i]));
                  }
                  jsonVal = new JSONObject(convertedKeyValPairs);
  
              } catch (IllegalArgumentException | IllegalAccessException e) {
                  throw ClassGraphException.newClassGraphException("Could not get value of field in object: " + obj,
                          e);
              }
          }
  
          // In the case of a DAG, just serialize the same object multiple times, i.e. remove obj
          // from visited set when exiting recursion, so that future instances also get serialized.
          visitedOnPath.remove(objKey);
  
          return jsonVal;
      }