public Set<String> getKeys(ObjectNode node) {
    Set<String> keys = new LinkedHashSet<>();
    if (node == null) {
      return keys;
    }

    Iterator<String> it = node.fieldNames();
    while (it.hasNext()) {
      keys.add(it.next());
    }

    return keys;
  }
  public ExternalDocs externalDocs(ObjectNode node, String location, ParseResult result) {
    ExternalDocs output = null;

    if (node != null) {
      output = new ExternalDocs();
      Set<String> keys = getKeys(node);

      String value = getString("description", node, false, location, result);
      output.description(value);

      value = getString("url", node, true, location, result);
      output.url(value);

      // extra keys
      for (String key : keys) {
        if (key.startsWith("x-")) {
          output.setVendorExtension(key, extension(node.get(key)));
        } else if (!EXTERNAL_DOCS_KEYS.contains(key)) {
          result.extra(location + ".externalDocs", key, node.get(key));
        }
      }
    }

    return output;
  }
  public Tag tag(ObjectNode node, String location, ParseResult result) {
    Tag tag = null;

    if (node != null) {
      tag = new Tag();
      Set<String> keys = getKeys(node);

      String value = getString("name", node, true, location, result);
      tag.name(value);

      value = getString("description", node, false, location, result);
      tag.description(value);

      ObjectNode externalDocs = getObject("externalDocs", node, false, location, result);
      ExternalDocs docs = externalDocs(externalDocs, location + "externalDocs", result);
      tag.externalDocs(docs);

      // extra keys
      for (String key : keys) {
        if (key.startsWith("x-")) {
          tag.setVendorExtension(key, extension(node.get(key)));
        } else if (!TAG_KEYS.contains(key)) {
          result.extra(location + ".externalDocs", key, node.get(key));
        }
      }
    }

    return tag;
  }
  public Response response(ObjectNode node, String location, ParseResult result) {
    if (node == null) return null;

    Response output = new Response();
    JsonNode ref = node.get("$ref");
    if (ref != null) {
      if (ref.getNodeType().equals(JsonNodeType.STRING)) {
        return refResponse((TextNode) ref, location, result);
      } else {
        result.invalidType(location, "$ref", "string", node);
        return null;
      }
    }

    String value = getString("description", node, true, location, result);
    output.description(value);

    ObjectNode schema = getObject("schema", node, false, location, result);
    if (schema != null) {
      output.schema(Json.mapper().convertValue(schema, Property.class));
    }
    ObjectNode headersNode = getObject("headers", node, false, location, result);
    if (headersNode != null) {
      // TODO
      Map<String, Property> headers =
          Json.mapper()
              .convertValue(
                  headersNode,
                  Json.mapper()
                      .getTypeFactory()
                      .constructMapType(Map.class, String.class, Property.class));
      output.headers(headers);
    }

    ObjectNode examplesNode = getObject("examples", node, false, location, result);
    if (examplesNode != null) {
      Map<String, Object> examples =
          Json.mapper()
              .convertValue(
                  examplesNode,
                  Json.mapper()
                      .getTypeFactory()
                      .constructMapType(Map.class, String.class, Object.class));
      output.setExamples(examples);
    }

    // extra keys
    Set<String> keys = getKeys(node);
    for (String key : keys) {
      if (key.startsWith("x-")) {
        output.setVendorExtension(key, extension(node.get(key)));
      } else if (!RESPONSE_KEYS.contains(key)) {
        result.extra(location, key, node.get(key));
      }
    }
    return output;
  }
  public RefModel refModel(ObjectNode node, String location, ParseResult result) {
    RefModel output = new RefModel();

    if (node.getNodeType().equals(JsonNodeType.OBJECT)) {
      String refValue = ((TextNode) node.get("$ref")).textValue();
      output.set$ref(refValue);
    } else {
      result.invalidType(location, "$ref", "object", node);
      return null;
    }

    // extra keys
    Set<String> keys = getKeys(node);
    for (String key : keys) {
      if (!REF_MODEL_KEYS.contains(key)) {
        result.extra(location, key, node.get(key));
      }
    }

    return output;
  }
  public License license(ObjectNode node, String location, ParseResult result) {
    if (node == null) return null;

    License license = new License();

    String value = getString("name", node, true, location, result);
    license.name(value);

    value = getString("url", node, false, location, result);
    license.url(value);

    // extra keys
    Set<String> keys = getKeys(node);
    for (String key : keys) {
      if (key.startsWith("x-")) {
        license.setVendorExtension(key, extension(node.get(key)));
      } else if (!LICENSE_KEYS.contains(key)) {
        result.extra(location + ".license", key, node.get(key));
      }
    }

    return license;
  }
  public Contact contact(ObjectNode node, String location, ParseResult result) {
    if (node == null) return null;

    Contact contact = new Contact();

    String value = getString("name", node, false, location + ".name", result);
    contact.name(value);

    value = getString("url", node, false, location + ".url", result);
    contact.url(value);

    value = getString("email", node, false, location + ".email", result);
    contact.email(value);

    // extra keys
    Set<String> keys = getKeys(node);
    for (String key : keys) {
      if (!CONTACT_KEYS.contains(key)) {
        result.extra(location + ".contact", key, node.get(key));
      }
    }

    return contact;
  }
  public Info info(ObjectNode node, String location, ParseResult result) {
    if (node == null) return null;

    Info info = new Info();
    String value = getString("title", node, true, location, result);
    info.title(value);

    value = getString("description", node, false, location, result);
    info.description(value);

    value = getString("termsOfService", node, false, location, result);
    info.termsOfService(value);

    ObjectNode obj = getObject("contact", node, false, "contact", result);
    Contact contact = contact(obj, location, result);
    info.contact(contact);

    obj = getObject("license", node, false, location, result);
    License license = license(obj, location, result);
    info.license(license);

    value = getString("version", node, false, location, result);
    info.version(value);

    // extra keys
    Set<String> keys = getKeys(node);
    for (String key : keys) {
      if (key.startsWith("x-")) {
        info.setVendorExtension(key, extension(node.get(key)));
      } else if (!INFO_KEYS.contains(key)) {
        result.extra(location, key, node.get(key));
      }
    }

    return info;
  }
  public Model definition(ObjectNode node, String location, ParseResult result) {
    if (node == null) {
      result.missing(location, "empty schema");
    }
    if (node.get("$ref") != null) {
      return refModel(node, location, result);
    }
    if (node.get("allOf") != null) {
      return allOfModel(node, location, result);
    }
    Model model = null;
    String value = null;

    String type = getString("type", node, false, location, result);
    Model m = new ModelImpl();
    if ("array".equals(type)) {
      ArrayModel am = new ArrayModel();
      ObjectNode propertyNode = getObject("properties", node, false, location, result);
      Map<String, Property> properties = properties(propertyNode, location, result);
      am.setProperties(properties);

      ObjectNode itemsNode = getObject("items", node, false, location, result);
      Property items = property(itemsNode, location, result);
      if (items != null) {
        am.items(items);
      }

      model = am;
    } else {
      ModelImpl impl = new ModelImpl();
      impl.setType(value);

      JsonNode ap = node.get("additionalProperties");
      if (ap != null && ap.getNodeType().equals(JsonNodeType.OBJECT)) {
        impl.setAdditionalProperties(Json.mapper().convertValue(ap, Property.class));
      }

      value = getString("default", node, false, location, result);
      impl.setDefaultValue(value);

      value = getString("format", node, false, location, result);
      impl.setFormat(value);

      value = getString("discriminator", node, false, location, result);
      impl.setDiscriminator(value);

      JsonNode xml = node.get("xml");
      if (xml != null) {
        impl.setXml(Json.mapper().convertValue(xml, Xml.class));
      }

      ObjectNode externalDocs = getObject("externalDocs", node, false, location, result);
      ExternalDocs docs = externalDocs(externalDocs, location, result);
      impl.setExternalDocs(docs);

      ObjectNode properties = getObject("properties", node, true, location, result);
      if (properties != null) {
        Set<String> propertyNames = getKeys(properties);
        for (String propertyName : propertyNames) {
          JsonNode propertyNode = properties.get(propertyName);
          if (propertyNode.getNodeType().equals(JsonNodeType.OBJECT)) {
            ObjectNode on = (ObjectNode) propertyNode;
            Property property = property(on, location, result);
            impl.property(propertyName, property);
          } else {
            result.invalidType(location, "properties", "object", propertyNode);
          }
        }
      }

      // need to set properties first
      ArrayNode required = getArray("required", node, false, location, result);
      if (required != null) {
        List<String> requiredProperties = new ArrayList<String>();
        for (JsonNode n : required) {
          if (n.getNodeType().equals(JsonNodeType.STRING)) {
            requiredProperties.add(((TextNode) n).textValue());
          } else {
            result.invalidType(location, "required", "string", n);
          }
        }
        if (requiredProperties.size() > 0) {
          impl.setRequired(requiredProperties);
        }
      }

      // extra keys
      Set<String> keys = getKeys(node);
      for (String key : keys) {
        if (key.startsWith("x-")) {
          impl.setVendorExtension(key, extension(node.get(key)));
        } else if (!SCHEMA_KEYS.contains(key)) {
          result.extra(location, key, node.get(key));
        }
      }
      if ("{ }".equals(Json.pretty(impl))) return null;
      model = impl;
    }
    JsonNode exampleNode = node.get("example");
    if (exampleNode != null) {
      // we support text or object nodes
      if (exampleNode.getNodeType().equals(JsonNodeType.OBJECT)) {
        ObjectNode on = getObject("example", node, false, location, result);
        if (on != null) {
          model.setExample(on);
        }
      } else {
        model.setExample(exampleNode.asText());
      }
    }

    if (model != null) {
      value = getString("description", node, false, location, result);
      model.setDescription(value);

      value = getString("title", node, false, location, result);
      model.setTitle(value);
    }

    return model;
  }
  public Parameter parameter(ObjectNode obj, String location, ParseResult result) {
    if (obj == null) {
      return null;
    }

    Parameter output = null;
    JsonNode ref = obj.get("$ref");
    if (ref != null) {
      if (ref.getNodeType().equals(JsonNodeType.STRING)) {
        return refParameter((TextNode) ref, location, result);
      } else {
        result.invalidType(location, "$ref", "string", obj);
        return null;
      }
    }

    String l = null;
    JsonNode ln = obj.get("name");
    if (ln != null) {
      l = ln.asText();
    } else {
      l = "['unknown']";
    }
    location += ".[" + l + "]";

    String value = getString("in", obj, true, location, result);
    if (value != null) {
      String type = getString("type", obj, false, location, result);
      String format = getString("format", obj, false, location, result);
      AbstractSerializableParameter<?> sp = null;
      if ("query".equals(value)) {
        sp = new QueryParameter();
      } else if ("header".equals(value)) {
        sp = new HeaderParameter();
      } else if ("path".equals(value)) {
        sp = new PathParameter();
      } else if ("formData".equals(value)) {
        sp = new FormParameter();
      }

      if (sp != null) {
        // type is mandatory when sp != null
        getString("type", obj, true, location, result);
        Map<PropertyBuilder.PropertyId, Object> map =
            new HashMap<PropertyBuilder.PropertyId, Object>();

        map.put(TYPE, type);
        map.put(FORMAT, format);
        String defaultValue = getString("default", obj, false, location, result);
        map.put(DEFAULT, defaultValue);
        sp.setDefault(defaultValue);

        Double dbl = getDouble("maximum", obj, false, location, result);
        if (dbl != null) {
          map.put(MAXIMUM, dbl);
          sp.setMaximum(dbl);
        }

        Boolean bl = getBoolean("exclusiveMaximum", obj, false, location, result);
        if (bl != null) {
          map.put(EXCLUSIVE_MAXIMUM, bl);
          sp.setExclusiveMaximum(bl);
        }

        dbl = getDouble("minimum", obj, false, location, result);
        if (dbl != null) {
          map.put(MINIMUM, dbl);
          sp.setMinimum(dbl);
        }

        bl = getBoolean("exclusiveMinimum", obj, false, location, result);
        if (bl != null) {
          map.put(EXCLUSIVE_MINIMUM, bl);
          sp.setExclusiveMinimum(bl);
        }

        map.put(MAX_LENGTH, getInteger("maxLength", obj, false, location, result));
        map.put(MIN_LENGTH, getInteger("minLength", obj, false, location, result));

        String pat = getString("pattern", obj, false, location, result);
        map.put(PATTERN, pat);
        sp.setPattern(pat);

        Integer iv = getInteger("maxItems", obj, false, location, result);
        map.put(MAX_ITEMS, iv);
        sp.setMaxItems(iv);

        iv = getInteger("minItems", obj, false, location, result);
        map.put(MIN_ITEMS, iv);
        sp.setMinItems(iv);

        map.put(UNIQUE_ITEMS, getBoolean("uniqueItems", obj, false, location, result));

        ArrayNode an = getArray("enum", obj, false, location, result);
        if (an != null) {
          List<String> _enum = new ArrayList<String>();
          for (JsonNode n : an) {
            _enum.add(n.textValue());
          }
          sp.setEnum(_enum);
          map.put(ENUM, _enum);
        }

        Property prop = PropertyBuilder.build(type, format, map);

        if (prop != null) {
          sp.setProperty(prop);
          ObjectNode items = getObject("items", obj, false, location, result);
          if (items != null) {
            Property inner = schema(null, items, location, result);
            sp.setItems(inner);
          }
        }

        Set<String> keys = getKeys(obj);
        for (String key : keys) {
          if (key.startsWith("x-")) {
            sp.setVendorExtension(key, extension(obj.get(key)));
          } else if (!PARAMETER_KEYS.contains(key)) {
            result.extra(location, key, obj.get(key));
          }
        }

        String collectionFormat = getString("collectionFormat", obj, false, location, result);
        sp.setCollectionFormat(collectionFormat);

        output = sp;
      } else if ("body".equals(value)) {
        output = Json.mapper().convertValue(obj, Parameter.class);
      }
      if (output != null) {
        value = getString("name", obj, true, location, result);
        output.setName(value);

        value = getString("description", obj, false, location, result);
        output.setDescription(value);

        Boolean required = getBoolean("required", obj, false, location, result);
        if (required != null) {
          output.setRequired(required);
        }
      }
    }

    return output;
  }
  public Operation operation(ObjectNode obj, String location, ParseResult result) {
    if (obj == null) {
      return null;
    }
    Operation output = new Operation();
    ArrayNode array = getArray("tags", obj, false, location, result);
    List<String> tags = tagStrings(array, location, result);
    if (tags != null) {
      output.tags(tags);
    }
    String value = getString("summary", obj, false, location, result);
    output.summary(value);

    value = getString("description", obj, false, location, result);
    output.description(value);

    ObjectNode externalDocs = getObject("externalDocs", obj, false, location, result);
    ExternalDocs docs = externalDocs(externalDocs, location, result);
    output.setExternalDocs(docs);

    value = getString("operationId", obj, false, location, result);
    output.operationId(value);

    array = getArray("consumes", obj, false, location, result);
    if (array != null) {
      if (array.size() == 0) {
        output.consumes(Collections.<String>emptyList());
      } else {
        Iterator<JsonNode> it = array.iterator();
        while (it.hasNext()) {
          JsonNode n = it.next();
          String s = getString(n, location + ".consumes", result);
          if (s != null) {
            output.consumes(s);
          }
        }
      }
    }
    array = getArray("produces", obj, false, location, result);
    if (array != null) {
      if (array.size() == 0) {
        output.produces(Collections.<String>emptyList());
      } else {
        Iterator<JsonNode> it = array.iterator();
        while (it.hasNext()) {
          JsonNode n = it.next();
          String s = getString(n, location + ".produces", result);
          if (s != null) {
            output.produces(s);
          }
        }
      }
    }
    ArrayNode parameters = getArray("parameters", obj, false, location, result);
    output.setParameters(parameters(parameters, location, result));

    ObjectNode responses = getObject("responses", obj, true, location, result);
    output.setResponses(responses(responses, location, result));

    array = getArray("schemes", obj, false, location, result);
    if (array != null) {
      Iterator<JsonNode> it = array.iterator();
      while (it.hasNext()) {
        JsonNode n = it.next();
        String s = getString(n, location + ".schemes", result);
        if (s != null) {
          Scheme scheme = Scheme.forValue(s);
          if (scheme != null) {
            output.scheme(scheme);
          }
        }
      }
    }
    Boolean deprecated = getBoolean("deprecated", obj, false, location, result);
    if (deprecated != null) {
      output.setDeprecated(deprecated);
    }
    array = getArray("security", obj, false, location, result);
    List<SecurityRequirement> security = securityRequirements(array, location, result);
    if (security != null) {
      List<Map<String, List<String>>> ss = new ArrayList<>();
      for (SecurityRequirement s : security) {
        if (s.getRequirements() != null && s.getRequirements().size() > 0) {
          ss.add(s.getRequirements());
        }
      }
      output.setSecurity(ss);
    }

    // extra keys
    Set<String> keys = getKeys(obj);
    for (String key : keys) {
      if (key.startsWith("x-")) {
        output.setVendorExtension(key, extension(obj.get(key)));
      } else if (!OPERATION_KEYS.contains(key)) {
        result.extra(location, key, obj.get(key));
      }
    }

    return output;
  }
  public Path path(ObjectNode obj, String location, ParseResult result) {
    boolean hasRef = false;
    Path output = null;
    if (obj.get("$ref") != null) {
      JsonNode ref = obj.get("$ref");
      if (ref.getNodeType().equals(JsonNodeType.STRING)) {
        return pathRef((TextNode) ref, location, result);
      } else if (ref.getNodeType().equals(JsonNodeType.OBJECT)) {
        ObjectNode on = (ObjectNode) ref;

        // extra keys
        Set<String> keys = getKeys(on);
        for (String key : keys) {
          result.extra(location, key, on.get(key));
        }
      }
      return null;
    }
    Path path = new Path();

    ArrayNode parameters = getArray("parameters", obj, false, location, result);
    path.setParameters(parameters(parameters, location, result));

    ObjectNode on = getObject("get", obj, false, location, result);
    if (on != null) {
      Operation op = operation(on, location + "(get)", result);
      if (op != null) {
        path.setGet(op);
      }
    }
    on = getObject("put", obj, false, location, result);
    if (on != null) {
      Operation op = operation(on, location + "(put)", result);
      if (op != null) {
        path.setPut(op);
      }
    }
    on = getObject("post", obj, false, location, result);
    if (on != null) {
      Operation op = operation(on, location + "(post)", result);
      if (op != null) {
        path.setPost(op);
      }
    }
    on = getObject("head", obj, false, location, result);
    if (on != null) {
      Operation op = operation(on, location + "(head)", result);
      if (op != null) {
        path.setHead(op);
      }
    }
    on = getObject("delete", obj, false, location, result);
    if (on != null) {
      Operation op = operation(on, location + "(delete)", result);
      if (op != null) {
        path.setDelete(op);
      }
    }
    on = getObject("patch", obj, false, location, result);
    if (on != null) {
      Operation op = operation(on, location + "(patch)", result);
      if (op != null) {
        path.setPatch(op);
      }
    }
    on = getObject("options", obj, false, location, result);
    if (on != null) {
      Operation op = operation(on, location + "(options)", result);
      if (op != null) {
        path.setOptions(op);
      }
    }

    // extra keys
    Set<String> keys = getKeys(obj);
    for (String key : keys) {
      if (key.startsWith("x-")) {
        path.setVendorExtension(key, extension(obj.get(key)));
      } else if (!PATH_KEYS.contains(key)) {
        result.extra(location, key, obj.get(key));
      }
    }
    return path;
  }
  public Swagger parseRoot(JsonNode node, ParseResult result) {
    String location = "";
    Swagger swagger = new Swagger();
    if (node.getNodeType().equals(JsonNodeType.OBJECT)) {
      ObjectNode on = (ObjectNode) node;
      Iterator<JsonNode> it = null;

      // required
      String value = getString("swagger", on, true, location, result);
      swagger.setSwagger(value);

      ObjectNode obj = getObject("info", on, true, "", result);
      if (obj != null) {
        Info info = info(obj, "info", result);
        swagger.info(info);
      }

      // optional
      value = getString("host", on, false, location, result);
      swagger.setHost(value);

      value = getString("basePath", on, false, location, result);
      swagger.setBasePath(value);

      ArrayNode array = getArray("schemes", on, false, location, result);
      if (array != null) {
        it = array.iterator();
        while (it.hasNext()) {
          JsonNode n = it.next();
          String s = getString(n, location + ".schemes", result);
          if (s != null) {
            Scheme scheme = Scheme.forValue(s);
            if (scheme != null) {
              swagger.scheme(scheme);
            }
          }
        }
      }

      array = getArray("consumes", on, false, location, result);
      if (array != null) {
        it = array.iterator();
        while (it.hasNext()) {
          JsonNode n = it.next();
          String s = getString(n, location + ".consumes", result);
          if (s != null) {
            swagger.consumes(s);
          }
        }
      }

      array = getArray("produces", on, false, location, result);
      if (array != null) {
        it = array.iterator();
        while (it.hasNext()) {
          JsonNode n = it.next();
          String s = getString(n, location + ".produces", result);
          if (s != null) {
            swagger.produces(s);
          }
        }
      }

      obj = getObject("paths", on, true, location, result);
      Map<String, Path> paths = paths(obj, "paths", result);
      swagger.paths(paths);

      obj = getObject("definitions", on, false, location, result);
      Map<String, Model> definitions = definitions(obj, "definitions", result);
      swagger.setDefinitions(definitions);

      obj = getObject("parameters", on, false, location, result);
      // TODO: parse
      Map<String, Parameter> parameters =
          Json.mapper()
              .convertValue(
                  obj,
                  Json.mapper()
                      .getTypeFactory()
                      .constructMapType(Map.class, String.class, Parameter.class));
      swagger.setParameters(parameters);

      obj = getObject("responses", on, false, location, result);
      Map<String, Response> responses = responses(obj, "responses", result);
      swagger.responses(responses);

      obj = getObject("securityDefinitions", on, false, location, result);
      Map<String, SecuritySchemeDefinition> securityDefinitions =
          securityDefinitions(obj, location, result);
      swagger.setSecurityDefinitions(securityDefinitions);

      array = getArray("security", on, false, location, result);
      List<SecurityRequirement> security = securityRequirements(array, location, result);
      swagger.setSecurity(security);

      array = getArray("tags", on, false, location, result);
      List<Tag> tags = tags(array, location, result);
      swagger.tags(tags);

      obj = getObject("externalDocs", on, false, location, result);
      ExternalDocs docs = externalDocs(obj, location, result);
      swagger.externalDocs(docs);

      // extra keys
      Set<String> keys = getKeys(on);
      for (String key : keys) {
        if (key.startsWith("x-")) {
          swagger.vendorExtension(key, extension(on.get(key)));
        } else if (!ROOT_KEYS.contains(key)) {
          result.extra(location, key, node.get(key));
        }
      }
    } else {
      result.invalidType("", "", "object", node);
      result.invalid();
      return null;
    }
    return swagger;
  }