private void generateField(FieldInfo field, Map<TypeSignature, String> visited, String path,
                               ObjectNode parent,
                               @Nullable ArrayNode required) {
        final ObjectNode fieldNode = mapper.createObjectNode();
        final TypeSignature fieldTypeSignature = field.typeSignature();

        fieldNode.put("description", field.descriptionInfo().docString());

        // Fill required fields for the current object.
        if (required != null && field.requirement() == FieldRequirement.REQUIRED) {
            required.add(field.name());
        }

        if (visited.containsKey(fieldTypeSignature)) {
            // If field is already visited, add a reference to the field instead of iterating its children.
            final String pathName = visited.get(fieldTypeSignature);
            fieldNode.put("$ref", pathName);
        } else {
            final String schemaType = getSchemaType(field.typeSignature());

            // Field is not visited, create a new type definition for it.
            fieldNode.put("type", schemaType);

            if (field.typeSignature().type() == TypeSignatureType.ENUM) {
                fieldNode.set("enum", getEnumType(field.typeSignature()));
            }

            final String currentPath;
            if (field.name().isEmpty()) {
                currentPath = path;
            } else {
                currentPath = path + '/' + field.name();
            }

            // Only Struct types map to custom objects to we need reference to those structs.
            // Having references to primitives do not make sense.
            if (MEMORIZED_JSON_TYPES.contains(schemaType)) {
                visited.put(fieldTypeSignature, currentPath);
            }

            // Based on field type, we need to call the appropriate method to generate the schema.
            // For example maps have `additionalProperties` field, arrays have `items` field and structs
            // have `properties` field.
            if (field.typeSignature().type() == TypeSignatureType.MAP) {
                generateMapFields(fieldNode, field, visited, currentPath);
            } else if (field.typeSignature().type() == TypeSignatureType.ITERABLE) {
                generateArrayFields(fieldNode, field, visited, currentPath);
            } else if ("object".equals(schemaType)) {
                generateStructFields(fieldNode, field, visited, currentPath);
            }
        }

        // Set current field inside the returned object.
        // If field is nameless, unpack it.
        // Example:
        // For `list<int> x` we should have `{"x": {"items": {"type": "integer"}}}`
        // Not `{"x": {"items": {"": {"type": "integer"}}}}`
        if (field.name().isEmpty()) {
            parent.setAll(fieldNode);
        } else {
            parent.set(field.name(), fieldNode);
        }
    }
Generate the JSON Schema for the given {@link FieldInfo} and add it to the given {@link ObjectNode}
and add required fields to the {@link ArrayNode}.

@param field field to generate schema for
@param visited map of visited types and their paths
@param path current path in tree traversal of fields
@param parent the parent to add schema properties
@param required the array node to add required field names, if parent doesn't support, it is null.
core/src/main/java/com/linecorp/armeria/server/docs/JsonSchemaGenerator.java