public static <T> T visit(final Schema start, final SchemaVisitor<T> visitor) {
    // Set of Visited Schemas
    IdentityHashMap<Schema, Schema> visited = new IdentityHashMap<>();
    // Stack that contains the Schams to process and afterVisitNonTerminal
    // functions.
    // Deque<Either<Schema, Supplier<SchemaVisitorAction>>>
    // Using either has a cost which we want to avoid...
    Deque<Object> dq = new ArrayDeque<>();
    dq.addLast(start);
    Object current;
    while ((current = dq.pollLast()) != null) {
      if (current instanceof Supplier) {
        // we are executing a non terminal post visit.
        SchemaVisitorAction action = ((Supplier<SchemaVisitorAction>) current).get();
        switch (action) {
        case CONTINUE:
          break;
        case SKIP_SUBTREE:
          throw new UnsupportedOperationException();
        case SKIP_SIBLINGS:
          while (dq.getLast() instanceof Schema) {
            dq.removeLast();
          }
          break;
        case TERMINATE:
          return visitor.get();
        default:
          throw new UnsupportedOperationException("Invalid action " + action);
        }
      } else {
        Schema schema = (Schema) current;
        boolean terminate;
        if (!visited.containsKey(schema)) {
          Schema.Type type = schema.getType();
          switch (type) {
          case ARRAY:
            terminate = visitNonTerminal(visitor, schema, dq, Collections.singleton(schema.getElementType()));
            visited.put(schema, schema);
            break;
          case RECORD:
            terminate = visitNonTerminal(visitor, schema, dq, () -> schema.getFields().stream().map(Field::schema)
                .collect(Collectors.toCollection(ArrayDeque::new)).descendingIterator());
            visited.put(schema, schema);
            break;
          case UNION:
            terminate = visitNonTerminal(visitor, schema, dq, schema.getTypes());
            visited.put(schema, schema);
            break;
          case MAP:
            terminate = visitNonTerminal(visitor, schema, dq, Collections.singleton(schema.getValueType()));
            visited.put(schema, schema);
            break;
          case NULL:
          case BOOLEAN:
          case BYTES:
          case DOUBLE:
          case ENUM:
          case FIXED:
          case FLOAT:
          case INT:
          case LONG:
          case STRING:
            terminate = visitTerminal(visitor, schema, dq);
            break;
          default:
            throw new UnsupportedOperationException("Invalid type " + type);
          }

        } else {
          terminate = visitTerminal(visitor, schema, dq);
        }
        if (terminate) {
          return visitor.get();
        }
      }
    }
    return visitor.get();
  }
@Test
  void visit6() {
    String s6 = "{\"type\": \"record\", \"name\": \"c1\", \"fields\": ["
        + "{\"name\": \"f1\", \"type\": {\"type\": \"record\", \"name\": \"ss2\", \"fields\": "
        + "[{\"name\": \"f11\", \"type\": \"int\"}]}}," + "{\"name\": \"f2\", \"type\": \"long\"}" + "]}";
    assertEquals("c1.ss2.!", Schemas.visit(new Schema.Parser().parse(s6), new TestVisitor()));

  }