From b88c66b154f72ca3e4d28fffc2088291793e06e0 Mon Sep 17 00:00:00 2001 From: TJKoury Date: Wed, 17 Dec 2025 13:42:05 -0500 Subject: [PATCH 1/5] first hack json --- include/flatbuffers/idl.h | 2 + src/flatc.cpp | 4 + src/idl_gen_json_schema.cpp | 228 ++++++++++++++++++++++++++++++++++++ tests/JsonSchemaTest.sh | 76 +++++++++++- 4 files changed, 304 insertions(+), 6 deletions(-) diff --git a/include/flatbuffers/idl.h b/include/flatbuffers/idl.h index 6e83eeffb69..b093a8dd634 100644 --- a/include/flatbuffers/idl.h +++ b/include/flatbuffers/idl.h @@ -735,6 +735,7 @@ struct IDLOptions { bool no_leak_private_annotations; bool require_json_eof; bool keep_proto_id; + bool jsonschema_include_xflatbuffers; /********************************** Python **********************************/ bool python_no_type_prefix_suffix; @@ -878,6 +879,7 @@ struct IDLOptions { no_leak_private_annotations(false), require_json_eof(true), keep_proto_id(false), + jsonschema_include_xflatbuffers(false), python_no_type_prefix_suffix(false), python_typing(false), python_gen_numpy(true), diff --git a/src/flatc.cpp b/src/flatc.cpp index 9cb0e4c2860..21aebba5777 100644 --- a/src/flatc.cpp +++ b/src/flatc.cpp @@ -260,6 +260,8 @@ const static FlatCOption flatc_options[] = { {"", "python-no-type-prefix-suffix", "", "Skip emission of Python functions that are prefixed with typenames"}, {"", "preserve-case", "", "Preserve all property cases as defined in IDL"}, + {"", "jsonschema-xflatbuffers", "", + "Include x-flatbuffers metadata in generated JSON Schema."}, {"", "python-typing", "", "Generate Python type annotations"}, {"", "python-version", "", "Generate code for the given Python version."}, {"", "python-decode-obj-api-strings", "", @@ -656,6 +658,8 @@ FlatCOptions FlatCompiler::ParseFromCommandLineArguments(int argc, opts.set_empty_vectors_to_null = false; } else if (arg == "--preserve-case") { options.preserve_case = true; + } else if (arg == "--jsonschema-xflatbuffers") { + opts.jsonschema_include_xflatbuffers = true; } else if (arg == "--java-primitive-has-method") { opts.java_primitive_has_method = true; } else if (arg == "--cs-gen-json-serializer") { diff --git a/src/idl_gen_json_schema.cpp b/src/idl_gen_json_schema.cpp index e24d6ce1235..706016441e9 100644 --- a/src/idl_gen_json_schema.cpp +++ b/src/idl_gen_json_schema.cpp @@ -162,6 +162,197 @@ class JsonSchemaGenerator : public BaseGenerator { private: std::string code_; + bool EmitXFlatbuffersMetadata() const { + return parser_.opts.jsonschema_include_xflatbuffers; + } + + std::string JsonString(const std::string& value) const { + std::string escaped; + if (EscapeString(value.c_str(), value.length(), &escaped, + parser_.opts.allow_non_utf8, parser_.opts.natural_utf8)) { + return escaped; + } + return "\"\""; + } + + static std::string JoinStrings(const std::vector& parts, + const std::string& separator) { + std::string output; + for (size_t i = 0; i < parts.size(); ++i) { + output += parts[i]; + if (i + 1 < parts.size()) output += separator; + } + return output; + } + + static std::string BaseTypeNameForMetadata(BaseType type) { + // Prefer schema-surface type names when available. + if (type == BASE_TYPE_UTYPE) return "utype"; + + const auto* name = TypeName(type); + if (name && *name) return name; + + switch (type) { + case BASE_TYPE_VECTOR: return "vector"; + case BASE_TYPE_VECTOR64: return "vector64"; + case BASE_TYPE_ARRAY: return "array"; + case BASE_TYPE_STRUCT: return "struct"; + case BASE_TYPE_UNION: return "union"; + default: return ""; + } + } + + std::string NamespaceArrayForMetadata(const Namespace* ns) const { + if (ns == nullptr || ns->components.empty()) return "[]"; + + std::vector components; + components.reserve(ns->components.size()); + for (const auto& component : ns->components) { + components.push_back(JsonString(component)); + } + return "[" + JoinStrings(components, ", ") + "]"; + } + + std::string TypeObjectForMetadata(const Type& type) const { + std::vector parts; + + auto add_kv = [&](const std::string& key, const std::string& value) { + parts.push_back("\"" + key + "\" : " + value); + }; + + if (type.base_type == BASE_TYPE_STRUCT && type.struct_def != nullptr) { + add_kv("base", JsonString(type.struct_def->fixed ? "struct" : "table")); + add_kv("ref", JsonString(GenFullName(type.struct_def))); + } else if (type.base_type == BASE_TYPE_UNION && type.enum_def != nullptr) { + add_kv("base", JsonString("union")); + add_kv("ref", JsonString(GenFullName(type.enum_def))); + } else if (type.base_type == BASE_TYPE_UTYPE && type.enum_def != nullptr) { + add_kv("base", JsonString("utype")); + add_kv("ref", JsonString(GenFullName(type.enum_def))); + } else if (type.base_type == BASE_TYPE_VECTOR || + type.base_type == BASE_TYPE_VECTOR64 || + type.base_type == BASE_TYPE_ARRAY) { + add_kv("base", JsonString(type.base_type == BASE_TYPE_ARRAY ? "array" + : "vector")); + if (type.base_type == BASE_TYPE_VECTOR64) add_kv("vector64", "true"); + if (type.base_type == BASE_TYPE_ARRAY) { + add_kv("fixed_length", NumToString(type.fixed_length)); + } + add_kv("element", TypeObjectForMetadata(type.VectorType())); + } else if (type.enum_def != nullptr) { + add_kv("base", JsonString("enum")); + add_kv("ref", JsonString(GenFullName(type.enum_def))); + add_kv("scalar", JsonString(BaseTypeNameForMetadata(type.base_type))); + } else { + add_kv("base", JsonString(BaseTypeNameForMetadata(type.base_type))); + } + + return "{ " + JoinStrings(parts, ", ") + " }"; + } + + std::string FieldObjectForMetadata(const FieldDef& field) const { + std::vector parts; + + auto add_kv = [&](const std::string& key, const std::string& value) { + parts.push_back("\"" + key + "\" : " + value); + }; + + add_kv("type", TypeObjectForMetadata(field.value.type)); + + const auto* id = field.attributes.Lookup("id"); + if (id != nullptr) { + int64_t id_value = 0; + if (StringToNumber(id->constant.c_str(), &id_value)) { + add_kv("id", NumToString(id_value)); + } else { + add_kv("id", JsonString(id->constant)); + } + } + + const char* presence = ""; + switch (field.presence) { + case FieldDef::kRequired: presence = "required"; break; + case FieldDef::kOptional: presence = "optional"; break; + case FieldDef::kDefault: presence = "default"; break; + } + add_kv("presence", JsonString(presence)); + + if (field.deprecated) add_kv("deprecated", "true"); + if (field.key) add_kv("key", "true"); + if (field.shared) add_kv("shared", "true"); + if (field.native_inline) add_kv("native_inline", "true"); + if (field.flexbuffer) add_kv("flexbuffer", "true"); + if (field.offset64) add_kv("offset64", "true"); + if (field.nested_flatbuffer != nullptr) { + add_kv("nested_flatbuffer", + JsonString(GenFullName(field.nested_flatbuffer))); + } + + if (field.sibling_union_field != nullptr) { + if (field.value.type.base_type == BASE_TYPE_UNION) { + add_kv("union_type_field", JsonString(field.sibling_union_field->name)); + } else if (field.value.type.base_type == BASE_TYPE_UTYPE) { + add_kv("union_value_field", + JsonString(field.sibling_union_field->name)); + } + } + + return "{ " + JoinStrings(parts, ", ") + " }"; + } + + std::string EnumObjectForMetadata(const EnumDef& enum_def) const { + std::vector parts; + + auto add_kv = [&](const std::string& key, const std::string& value) { + parts.push_back("\"" + key + "\" : " + value); + }; + + add_kv("kind", JsonString(enum_def.is_union ? "union" : "enum")); + add_kv("name", JsonString(enum_def.name)); + add_kv("namespace", NamespaceArrayForMetadata(enum_def.defined_namespace)); + add_kv("underlying_type", + JsonString(BaseTypeNameForMetadata(enum_def.underlying_type.base_type))); + + std::vector values; + values.reserve(enum_def.Vals().size()); + for (const auto* enum_val : enum_def.Vals()) { + std::vector value_parts; + value_parts.push_back("\"name\" : " + JsonString(enum_val->name)); + value_parts.push_back("\"value\" : " + + JsonString(enum_def.ToString(*enum_val))); + if (enum_def.is_union && enum_val->union_type.base_type != BASE_TYPE_NONE) { + if (enum_val->union_type.struct_def != nullptr) { + value_parts.push_back("\"union_type\" : " + + JsonString(GenFullName(enum_val->union_type.struct_def))); + } + } + values.push_back("{ " + JoinStrings(value_parts, ", ") + " }"); + } + add_kv("values", "[ " + JoinStrings(values, ", ") + " ]"); + + return "{ " + JoinStrings(parts, ", ") + " }"; + } + + std::string StructObjectForMetadata(const StructDef& struct_def) const { + std::vector parts; + + auto add_kv = [&](const std::string& key, const std::string& value) { + parts.push_back("\"" + key + "\" : " + value); + }; + + add_kv("kind", JsonString(struct_def.fixed ? "struct" : "table")); + add_kv("name", JsonString(struct_def.name)); + add_kv("namespace", + NamespaceArrayForMetadata(struct_def.defined_namespace)); + if (struct_def.has_key) add_kv("has_key", "true"); + if (struct_def.fixed) { + add_kv("minalign", NumToString(struct_def.minalign)); + add_kv("bytesize", NumToString(struct_def.bytesize)); + } + + return "{ " + JoinStrings(parts, ", ") + " }"; + } + public: JsonSchemaGenerator(const Parser& parser, const std::string& path, const std::string& file_name) @@ -231,11 +422,37 @@ class JsonSchemaGenerator : public BaseGenerator { code_ += Indent(1) + "\"$schema\": \"https://json-schema.org/draft/2019-09/schema\"," + NewLine(); + if (EmitXFlatbuffersMetadata()) { + std::vector> xfb_keys; + xfb_keys.emplace_back("root_type", + JsonString(GenFullName(parser_.root_struct_def_))); + if (!parser_.file_identifier_.empty()) { + xfb_keys.emplace_back("file_identifier", + JsonString(parser_.file_identifier_)); + } + if (!parser_.file_extension_.empty()) { + xfb_keys.emplace_back("file_extension", + JsonString(parser_.file_extension_)); + } + + code_ += Indent(1) + "\"x-flatbuffers\" : {" + NewLine(); + for (size_t i = 0; i < xfb_keys.size(); ++i) { + code_ += Indent(2) + "\"" + xfb_keys[i].first + "\" : " + + xfb_keys[i].second; + if (i + 1 < xfb_keys.size()) code_ += ","; + code_ += NewLine(); + } + code_ += Indent(1) + "}," + NewLine(); + } code_ += Indent(1) + "\"definitions\": {" + NewLine(); for (auto e = parser_.enums_.vec.cbegin(); e != parser_.enums_.vec.cend(); ++e) { code_ += Indent(2) + "\"" + GenFullName(*e) + "\" : {" + NewLine(); code_ += Indent(3) + GenType("string") + "," + NewLine(); + if (EmitXFlatbuffersMetadata()) { + code_ += Indent(3) + "\"x-flatbuffers\" : " + + EnumObjectForMetadata(**e) + "," + NewLine(); + } auto enumdef(Indent(3) + "\"enum\": ["); for (auto enum_value = (*e)->Vals().begin(); enum_value != (*e)->Vals().end(); ++enum_value) { @@ -258,6 +475,10 @@ class JsonSchemaGenerator : public BaseGenerator { if (comment != "") { code_ += Indent(3) + "\"description\" : " + comment + "," + NewLine(); } + if (EmitXFlatbuffersMetadata()) { + code_ += Indent(3) + "\"x-flatbuffers\" : " + + StructObjectForMetadata(*structure) + "," + NewLine(); + } code_ += Indent(3) + "\"properties\" : {" + NewLine(); @@ -276,11 +497,18 @@ class JsonSchemaGenerator : public BaseGenerator { deprecated_info = "," + NewLine() + Indent(8) + "\"deprecated\" : true"; } + std::string flatbuffers_info = ""; + if (EmitXFlatbuffersMetadata()) { + flatbuffers_info = + "," + NewLine() + Indent(8) + "\"x-flatbuffers\" : " + + FieldObjectForMetadata(*property); + } std::string typeLine = Indent(4) + "\"" + property->name + "\""; typeLine += " : {" + NewLine() + Indent(8); typeLine += GenType(property->value.type); typeLine += arrayInfo; typeLine += deprecated_info; + typeLine += flatbuffers_info; auto description = PrepareDescription(property->doc_comment); if (description != "") { typeLine += diff --git a/tests/JsonSchemaTest.sh b/tests/JsonSchemaTest.sh index 97e9381de4a..fe11f00ab72 100755 --- a/tests/JsonSchemaTest.sh +++ b/tests/JsonSchemaTest.sh @@ -3,7 +3,10 @@ set -eu script_dir="$(cd "$(dirname "$0")" && pwd)" repo_dir="$(cd "${script_dir}/.." && pwd)" -flatc="${repo_dir}/flatc" +flatc="${repo_dir}/build/flatc" +if [[ ! -x "${flatc}" ]]; then + flatc="${repo_dir}/flatc" +fi if [[ ! -x "${flatc}" ]]; then echo "Skipping JSON Schema tests: flatc executable not found at ${flatc}." >&2 @@ -35,7 +38,52 @@ compare_output() { done } -run_case() { +compare_output_stripping_xflatbuffers() { + local out_dir="$1" + for golden in "${golden_files[@]}"; do + local golden_path="${script_dir}/${golden}" + local generated_path="${out_dir}/${golden}" + python3 - "${golden_path}" "${generated_path}" <<'PY' +import json +import sys + + +def count_xflatbuffers(obj) -> int: + if isinstance(obj, dict): + count = 1 if "x-flatbuffers" in obj else 0 + return count + sum(count_xflatbuffers(v) for v in obj.values()) + if isinstance(obj, list): + return sum(count_xflatbuffers(v) for v in obj) + return 0 + + +def strip_xflatbuffers(obj): + if isinstance(obj, dict): + return {k: strip_xflatbuffers(v) for k, v in obj.items() if k != "x-flatbuffers"} + if isinstance(obj, list): + return [strip_xflatbuffers(v) for v in obj] + return obj + + +golden_path, generated_path = sys.argv[1], sys.argv[2] +with open(golden_path, "r", encoding="utf-8") as f: + golden = json.load(f) +with open(generated_path, "r", encoding="utf-8") as f: + generated = json.load(f) + +xfb_count = count_xflatbuffers(generated) +if xfb_count == 0: + print(f"Expected x-flatbuffers metadata in {generated_path}", file=sys.stderr) + sys.exit(1) + +if strip_xflatbuffers(golden) != strip_xflatbuffers(generated): + print(f"JSON Schema mismatch (ignoring x-flatbuffers) for {generated_path}", file=sys.stderr) + sys.exit(1) +PY + done +} + +run_case_diff() { local label="$1" local out_dir="$2" shift 2 @@ -47,14 +95,30 @@ run_case() { compare_output "${out_dir}" } +run_case_xflatbuffers() { + local label="$1" + local out_dir="$2" + shift 2 + + echo "Generating JSON Schemas (${label})" + rm -rf "${out_dir}" + mkdir -p "${out_dir}" + ( cd "${script_dir}" && "${flatc}" "$@" "${include_flags[@]}" -o "${out_dir}" "${schemas[@]}" ) + compare_output_stripping_xflatbuffers "${out_dir}" +} + tmp_default="$(mktemp -d)" tmp_preserve="$(mktemp -d)" +tmp_xflatbuffers_default="$(mktemp -d)" +tmp_xflatbuffers_preserve="$(mktemp -d)" cleanup() { - rm -rf "${tmp_default}" "${tmp_preserve}" + rm -rf "${tmp_default}" "${tmp_preserve}" "${tmp_xflatbuffers_default}" "${tmp_xflatbuffers_preserve}" } trap cleanup EXIT -run_case "default naming" "${tmp_default}" --jsonschema -run_case "preserve-case naming" "${tmp_preserve}" --jsonschema --preserve-case +run_case_diff "default naming" "${tmp_default}" --jsonschema +run_case_diff "preserve-case naming" "${tmp_preserve}" --jsonschema --preserve-case +run_case_xflatbuffers "x-flatbuffers metadata" "${tmp_xflatbuffers_default}" --jsonschema --jsonschema-xflatbuffers +run_case_xflatbuffers "x-flatbuffers metadata + preserve-case" "${tmp_xflatbuffers_preserve}" --jsonschema --jsonschema-xflatbuffers --preserve-case -echo "JSON Schema tests (default + preserve-case) passed" +echo "JSON Schema tests (default + preserve-case + x-flatbuffers) passed" From 3a827f14f5bbac89e5f7f2d5cdb3826f00395ca8 Mon Sep 17 00:00:00 2001 From: TJKoury Date: Wed, 17 Dec 2025 19:34:14 -0500 Subject: [PATCH 2/5] added json parser --- include/flatbuffers/idl.h | 6 + src/flatc.cpp | 35 +- src/idl_parser.cpp | 1144 +++++++++++++++++++++++++++++++++++++ tests/JsonSchemaTest.sh | 52 +- 4 files changed, 1230 insertions(+), 7 deletions(-) diff --git a/include/flatbuffers/idl.h b/include/flatbuffers/idl.h index b093a8dd634..97453f07443 100644 --- a/include/flatbuffers/idl.h +++ b/include/flatbuffers/idl.h @@ -1070,6 +1070,11 @@ class Parser : public ParserState { bool ParseJson(const char* json, const char* json_filename = nullptr); + // Parse a JSON Schema (as produced by `flatc --jsonschema`) into the + // FlatBuffers schema IR. + bool ParseJsonSchema(const char* json_schema, + const char* json_schema_filename = nullptr); + // Returns the number of characters were consumed when parsing a JSON string. std::ptrdiff_t BytesConsumed() const; @@ -1216,6 +1221,7 @@ class Parser : public ParserState { const char* source_filename, const char* include_filename); FLATBUFFERS_CHECKED_ERROR DoParseJson(); + FLATBUFFERS_CHECKED_ERROR DoParseJsonSchema(); FLATBUFFERS_CHECKED_ERROR CheckClash(std::vector& fields, StructDef* struct_def, const char* suffix, BaseType baseType); diff --git a/src/flatc.cpp b/src/flatc.cpp index 21aebba5777..03639fd7ecd 100644 --- a/src/flatc.cpp +++ b/src/flatc.cpp @@ -38,6 +38,21 @@ void FlatCompiler::ParseFile( flatbuffers::Parser& parser, const std::string& filename, const std::string& contents, const std::vector& include_directories) const { + const bool is_json_schema = + filename.size() >= strlen(".schema.json") && + filename.compare(filename.size() - strlen(".schema.json"), + strlen(".schema.json"), ".schema.json") == 0; + + if (is_json_schema) { + if (!parser.ParseJsonSchema(contents.c_str(), filename.c_str())) { + Error(parser.error_, false, false); + } + if (!parser.error_.empty()) { + Warn(parser.error_, false); + } + return; + } + auto local_include_directory = flatbuffers::StripFileName(filename); std::vector inc_directories; @@ -399,8 +414,9 @@ std::string FlatCompiler::GetUsageString( ss << "\n"; std::string files_description = - "FILEs may be schemas (must end in .fbs), binary schemas (must end in " - ".bfbs) or JSON files (conforming to preceding schema). BINARY_FILEs " + "FILEs may be schemas (must end in .fbs, .proto, or .schema.json), " + "binary schemas (must end in .bfbs) or JSON files (conforming to " + "preceding schema). BINARY_FILEs " "after the -- must be binary flatbuffer format files. Output files are " "named using the base file name of the input, and written to the current " "directory or the path given by -o. example: " + @@ -887,7 +903,11 @@ std::unique_ptr FlatCompiler::GenerateCode(const FlatCOptions& options, bool is_binary = static_cast(file_it - options.filenames.begin()) >= options.binary_files_from; auto ext = flatbuffers::GetExtension(filename); - const bool is_schema = ext == "fbs" || ext == "proto"; + const bool is_json_schema = + filename.size() >= strlen(".schema.json") && + filename.compare(filename.size() - strlen(".schema.json"), + strlen(".schema.json"), ".schema.json") == 0; + const bool is_schema = is_json_schema || ext == "fbs" || ext == "proto"; if (is_schema && opts.project_root.empty()) { opts.project_root = StripFileName(filename); } @@ -967,8 +987,13 @@ std::unique_ptr FlatCompiler::GenerateCode(const FlatCOptions& options, parser->file_extension_ = reflection::SchemaExtension(); } } - std::string filebase = - flatbuffers::StripPath(flatbuffers::StripExtension(filename)); + std::string filebase = flatbuffers::StripPath(filename); + if (is_json_schema) { + filebase = + filebase.substr(0, filebase.size() - strlen(".schema.json")); + } else { + filebase = flatbuffers::StripExtension(filebase); + } // If one of the generators uses bfbs, serialize the parser and get // the serialized buffer and length. diff --git a/src/idl_parser.cpp b/src/idl_parser.cpp index a1530db56df..001ea70b49d 100644 --- a/src/idl_parser.cpp +++ b/src/idl_parser.cpp @@ -3578,6 +3578,16 @@ bool Parser::ParseJson(const char* json, const char* json_filename) { return done; } +bool Parser::ParseJsonSchema(const char* json_schema, + const char* json_schema_filename) { + const auto initial_depth = parse_depth_counter_; + (void)initial_depth; + const auto done = !StartParseFile(json_schema, json_schema_filename).Check() && + !DoParseJsonSchema().Check(); + FLATBUFFERS_ASSERT(initial_depth == parse_depth_counter_); + return done; +} + std::ptrdiff_t Parser::BytesConsumed() const { return std::distance(source_, prev_cursor_); } @@ -3935,6 +3945,1140 @@ CheckedError Parser::DoParseJson() { return NoError(); } +CheckedError Parser::DoParseJsonSchema() { + const char* const schema_source = source_; + const std::string schema_filename = file_being_parsed_; + + struct EnumValueMeta { + std::string name; + std::string value; + std::string union_type; + }; + + struct DefinitionXfbMeta { + bool present = false; + std::string kind; + std::string name; + std::vector name_space; + std::string underlying_type; + std::vector values; + bool has_key = false; + }; + + struct FieldXfbMeta { + bool present = false; + bool has_type = false; + Type type; + bool has_id = false; + std::string id; + bool has_presence = false; + FieldDef::Presence presence = FieldDef::kDefault; + bool has_deprecated = false; + bool deprecated = false; + bool has_key = false; + bool key = false; + bool has_shared = false; + bool shared = false; + bool has_native_inline = false; + bool native_inline = false; + bool has_flexbuffer = false; + bool flexbuffer = false; + bool has_offset64 = false; + bool offset64 = false; + std::string nested_flatbuffer; + std::string union_type_field; + std::string union_value_field; + }; + + struct ParsedField { + std::string name; + Type type; + bool deprecated = false; + std::string description; + FieldXfbMeta xfb; + bool is_anyof = false; + std::vector anyof_types; + }; + + auto ParseString = [&](std::string* out) -> CheckedError { + if (!Is(kTokenStringConstant)) return Error("string constant expected"); + *out = attribute_; + EXPECT(kTokenStringConstant); + return NoError(); + }; + + auto ParseBool = [&](bool* out) -> CheckedError { + if (IsIdent("true")) { + *out = true; + NEXT(); + return NoError(); + } + if (IsIdent("false")) { + *out = false; + NEXT(); + return NoError(); + } + return Error("boolean constant expected"); + }; + + auto ParseInt64 = [&](int64_t* out) -> CheckedError { + if (token_ != kTokenIntegerConstant) return Error("integer constant expected"); + if (!StringToNumber(attribute_.c_str(), out)) { + return Error("invalid integer constant: " + attribute_); + } + NEXT(); + return NoError(); + }; + + auto ParseUInt64 = [&](uint64_t* out) -> CheckedError { + if (token_ != kTokenIntegerConstant) return Error("integer constant expected"); + if (!StringToNumber(attribute_.c_str(), out)) { + return Error("invalid integer constant: " + attribute_); + } + NEXT(); + return NoError(); + }; + + auto ParseStringArray = [&](std::vector* out) -> CheckedError { + out->clear(); + size_t count = 0; + auto err = ParseVectorDelimiters(count, [&](size_t&) -> CheckedError { + std::string value; + ECHECK(ParseString(&value)); + out->push_back(std::move(value)); + return NoError(); + }); + ECHECK(err); + return NoError(); + }; + + auto ParseNamespaceArray = [&](std::vector* out) -> CheckedError { + return ParseStringArray(out); + }; + + auto SplitLines = [&](const std::string& text) { + std::vector lines; + size_t start = 0; + while (start <= text.size()) { + auto end = text.find('\n', start); + if (end == std::string::npos) end = text.size(); + lines.push_back(text.substr(start, end - start)); + if (end == text.size()) break; + start = end + 1; + } + return lines; + }; + + auto MakeSchemaFullName = [&](const std::vector& ns, + const std::string& name) { + std::string full_name; + for (const auto& component : ns) { + full_name.append(component); + full_name.push_back('_'); + } + full_name.append(name); + return full_name; + }; + + auto ExtractDefinitionRef = [&](const std::string& ref, + std::string* out) -> CheckedError { + static const std::string kPrefix = "#/definitions/"; + if (ref.rfind(kPrefix, 0) != 0) return Error("unsupported $ref: " + ref); + *out = ref.substr(kPrefix.size()); + if (out->empty()) return Error("unsupported $ref: " + ref); + return NoError(); + }; + + auto BaseTypeFromScalarName = [&](const std::string& name, + BaseType* out) -> bool { + if (name == "bool") { + *out = BASE_TYPE_BOOL; + return true; + } else if (name == "byte") { + *out = BASE_TYPE_CHAR; + return true; + } else if (name == "ubyte") { + *out = BASE_TYPE_UCHAR; + return true; + } else if (name == "short") { + *out = BASE_TYPE_SHORT; + return true; + } else if (name == "ushort") { + *out = BASE_TYPE_USHORT; + return true; + } else if (name == "int") { + *out = BASE_TYPE_INT; + return true; + } else if (name == "uint") { + *out = BASE_TYPE_UINT; + return true; + } else if (name == "long") { + *out = BASE_TYPE_LONG; + return true; + } else if (name == "ulong") { + *out = BASE_TYPE_ULONG; + return true; + } else if (name == "float") { + *out = BASE_TYPE_FLOAT; + return true; + } else if (name == "double") { + *out = BASE_TYPE_DOUBLE; + return true; + } else if (name == "string") { + *out = BASE_TYPE_STRING; + return true; + } else if (name == "utype") { + *out = BASE_TYPE_UTYPE; + return true; + } + return false; + }; + + auto InferIntegerBaseType = [&](int64_t min_value, uint64_t max_value, + BaseType* out) -> bool { + if (min_value == flatbuffers::numeric_limits::min() && + max_value == static_cast( + flatbuffers::numeric_limits::max())) { + *out = BASE_TYPE_CHAR; + return true; + } + if (min_value == 0 && + max_value == flatbuffers::numeric_limits::max()) { + *out = BASE_TYPE_UCHAR; + return true; + } + if (min_value == flatbuffers::numeric_limits::min() && + max_value == static_cast( + flatbuffers::numeric_limits::max())) { + *out = BASE_TYPE_SHORT; + return true; + } + if (min_value == 0 && + max_value == flatbuffers::numeric_limits::max()) { + *out = BASE_TYPE_USHORT; + return true; + } + if (min_value == flatbuffers::numeric_limits::min() && + max_value == static_cast( + flatbuffers::numeric_limits::max())) { + *out = BASE_TYPE_INT; + return true; + } + if (min_value == 0 && + max_value == flatbuffers::numeric_limits::max()) { + *out = BASE_TYPE_UINT; + return true; + } + if (min_value == flatbuffers::numeric_limits::min() && + max_value == static_cast( + flatbuffers::numeric_limits::max())) { + *out = BASE_TYPE_LONG; + return true; + } + if (min_value == 0 && + max_value == flatbuffers::numeric_limits::max()) { + *out = BASE_TYPE_ULONG; + return true; + } + return false; + }; + + std::map enum_by_fullname; + std::map struct_by_fullname; + + auto ParseXfbType = [&](Type* out_type, + auto&& ParseXfbTypeRef) -> CheckedError { + std::string base; + std::string ref; + std::string scalar; + bool vector64 = false; + bool has_vector64 = false; + uint16_t fixed_length = 0; + bool has_fixed_length = false; + Type element_type; + bool has_element = false; + + size_t fieldn = 0; + auto err = ParseTableDelimiters( + fieldn, nullptr, + [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { + if (key == "base") { + ECHECK(ParseString(&base)); + } else if (key == "ref") { + ECHECK(ParseString(&ref)); + } else if (key == "scalar") { + ECHECK(ParseString(&scalar)); + } else if (key == "vector64") { + has_vector64 = true; + ECHECK(ParseBool(&vector64)); + } else if (key == "fixed_length") { + int64_t v = 0; + ECHECK(ParseInt64(&v)); + if (v < 0 || v > flatbuffers::numeric_limits::max()) { + return Error("fixed_length out of range"); + } + fixed_length = static_cast(v); + has_fixed_length = true; + } else if (key == "element") { + has_element = true; + ECHECK(ParseXfbTypeRef(&element_type, ParseXfbTypeRef)); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(err); + + if (base.empty()) return Error("x-flatbuffers type missing 'base'"); + + if (base == "struct" || base == "table") { + auto it = struct_by_fullname.find(ref); + if (it == struct_by_fullname.end()) { + return Error("unknown struct ref in x-flatbuffers type: " + ref); + } + *out_type = Type(BASE_TYPE_STRUCT, it->second); + if (base == "struct") it->second->fixed = true; + return NoError(); + } + if (base == "union") { + auto it = enum_by_fullname.find(ref); + if (it == enum_by_fullname.end()) { + return Error("unknown enum ref in x-flatbuffers type: " + ref); + } + it->second->is_union = true; + it->second->underlying_type.base_type = BASE_TYPE_UTYPE; + it->second->underlying_type.enum_def = it->second; + *out_type = Type(BASE_TYPE_UNION, nullptr, it->second); + return NoError(); + } + if (base == "utype") { + auto it = enum_by_fullname.find(ref); + if (it == enum_by_fullname.end()) { + return Error("unknown enum ref in x-flatbuffers type: " + ref); + } + it->second->is_union = true; + it->second->underlying_type.base_type = BASE_TYPE_UTYPE; + it->second->underlying_type.enum_def = it->second; + *out_type = Type(BASE_TYPE_UTYPE, nullptr, it->second); + return NoError(); + } + if (base == "enum") { + auto it = enum_by_fullname.find(ref); + if (it == enum_by_fullname.end()) { + return Error("unknown enum ref in x-flatbuffers type: " + ref); + } + BaseType bt = BASE_TYPE_INT; + if (!scalar.empty() && !BaseTypeFromScalarName(scalar, &bt)) { + return Error("unknown scalar type in x-flatbuffers enum: " + scalar); + } + it->second->underlying_type.base_type = bt; + it->second->underlying_type.enum_def = it->second; + *out_type = Type(bt, nullptr, it->second); + return NoError(); + } + if (base == "vector" || base == "array") { + if (!has_element) return Error("x-flatbuffers type missing 'element'"); + auto vector_base = + (has_vector64 && vector64) ? BASE_TYPE_VECTOR64 : BASE_TYPE_VECTOR; + if (base == "array") vector_base = BASE_TYPE_ARRAY; + + *out_type = Type(vector_base, element_type.struct_def, + element_type.enum_def, + vector_base == BASE_TYPE_ARRAY ? fixed_length : 0); + out_type->element = element_type.base_type; + if (vector_base == BASE_TYPE_ARRAY && !has_fixed_length) { + return Error("x-flatbuffers array missing 'fixed_length'"); + } + return NoError(); + } + + BaseType bt = BASE_TYPE_NONE; + if (!BaseTypeFromScalarName(base, &bt)) { + return Error("unknown scalar type in x-flatbuffers type: " + base); + } + *out_type = Type(bt); + return NoError(); + }; + + auto ParseDefinitionXfbMeta = [&](DefinitionXfbMeta* meta) -> CheckedError { + meta->present = true; + size_t fieldn = 0; + auto err = ParseTableDelimiters( + fieldn, nullptr, + [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { + if (key == "kind") { + ECHECK(ParseString(&meta->kind)); + } else if (key == "name") { + ECHECK(ParseString(&meta->name)); + } else if (key == "namespace") { + ECHECK(ParseNamespaceArray(&meta->name_space)); + } else if (key == "underlying_type") { + ECHECK(ParseString(&meta->underlying_type)); + } else if (key == "has_key") { + ECHECK(ParseBool(&meta->has_key)); + } else if (key == "values") { + size_t count = 0; + meta->values.clear(); + auto values_err = + ParseVectorDelimiters(count, [&](size_t&) -> CheckedError { + EnumValueMeta value_meta; + size_t vn = 0; + auto obj_err = ParseTableDelimiters( + vn, nullptr, + [&](const std::string& vk, size_t&, + const StructDef*) -> CheckedError { + if (vk == "name") { + ECHECK(ParseString(&value_meta.name)); + } else if (vk == "value") { + ECHECK(ParseString(&value_meta.value)); + } else if (vk == "union_type") { + ECHECK(ParseString(&value_meta.union_type)); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(obj_err); + meta->values.push_back(std::move(value_meta)); + return NoError(); + }); + ECHECK(values_err); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(err); + return NoError(); + }; + + auto ParseFieldXfbMeta = [&](FieldXfbMeta* meta) -> CheckedError { + meta->present = true; + size_t fieldn = 0; + auto err = ParseTableDelimiters( + fieldn, nullptr, + [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { + if (key == "type") { + meta->has_type = true; + ECHECK(ParseXfbType(&meta->type, ParseXfbType)); + } else if (key == "id") { + meta->has_id = true; + if (Is(kTokenStringConstant)) { + ECHECK(ParseString(&meta->id)); + } else if (token_ == kTokenIntegerConstant) { + meta->id = attribute_; + NEXT(); + } else { + return Error("unexpected id value in x-flatbuffers metadata"); + } + } else if (key == "presence") { + meta->has_presence = true; + std::string presence; + ECHECK(ParseString(&presence)); + if (presence == "required") { + meta->presence = FieldDef::kRequired; + } else if (presence == "optional") { + meta->presence = FieldDef::kOptional; + } else if (presence == "default") { + meta->presence = FieldDef::kDefault; + } else { + return Error("unknown presence: " + presence); + } + } else if (key == "deprecated") { + meta->has_deprecated = true; + ECHECK(ParseBool(&meta->deprecated)); + } else if (key == "key") { + meta->has_key = true; + ECHECK(ParseBool(&meta->key)); + } else if (key == "shared") { + meta->has_shared = true; + ECHECK(ParseBool(&meta->shared)); + } else if (key == "native_inline") { + meta->has_native_inline = true; + ECHECK(ParseBool(&meta->native_inline)); + } else if (key == "flexbuffer") { + meta->has_flexbuffer = true; + ECHECK(ParseBool(&meta->flexbuffer)); + } else if (key == "offset64") { + meta->has_offset64 = true; + ECHECK(ParseBool(&meta->offset64)); + } else if (key == "nested_flatbuffer") { + ECHECK(ParseString(&meta->nested_flatbuffer)); + } else if (key == "union_type_field") { + ECHECK(ParseString(&meta->union_type_field)); + } else if (key == "union_value_field") { + ECHECK(ParseString(&meta->union_value_field)); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(err); + return NoError(); + }; + + auto ApplyFieldXfb = [&](FieldDef& field, + const FieldXfbMeta& xfb_field) -> CheckedError { + if (!xfb_field.present) return NoError(); + + if (xfb_field.has_id) { + auto* v = new Value(); + v->constant = xfb_field.id; + field.attributes.Add("id", v); + } + if (xfb_field.has_presence) field.presence = xfb_field.presence; + if (xfb_field.has_deprecated) field.deprecated = xfb_field.deprecated; + if (xfb_field.has_key) field.key = xfb_field.key; + if (xfb_field.has_shared) field.shared = xfb_field.shared; + if (xfb_field.has_native_inline) field.native_inline = xfb_field.native_inline; + if (xfb_field.has_flexbuffer) field.flexbuffer = xfb_field.flexbuffer; + if (xfb_field.has_offset64) field.offset64 = xfb_field.offset64; + + if (!xfb_field.nested_flatbuffer.empty()) { + auto it = struct_by_fullname.find(xfb_field.nested_flatbuffer); + if (it == struct_by_fullname.end()) { + return Error("unknown nested_flatbuffer: " + xfb_field.nested_flatbuffer); + } + field.nested_flatbuffer = it->second; + } + + return NoError(); + }; + + auto ResolveRefType = [&](const std::string& ref_fullname, + Type* out_type) -> CheckedError { + auto eit = enum_by_fullname.find(ref_fullname); + if (eit != enum_by_fullname.end()) { + *out_type = eit->second->underlying_type; + return NoError(); + } + auto sit = struct_by_fullname.find(ref_fullname); + if (sit != struct_by_fullname.end()) { + *out_type = Type(BASE_TYPE_STRUCT, sit->second); + return NoError(); + } + return Error("unknown $ref: " + ref_fullname); + }; + + auto ParseAnyOfRefs = [&](std::vector* out) -> CheckedError { + out->clear(); + size_t count = 0; + auto err = ParseVectorDelimiters(count, [&](size_t&) -> CheckedError { + std::string ref_fullname; + size_t fn = 0; + auto obj_err = ParseTableDelimiters( + fn, nullptr, + [&](const std::string& k, size_t&, const StructDef*) -> CheckedError { + if (k == "$ref") { + std::string ref; + ECHECK(ParseString(&ref)); + ECHECK(ExtractDefinitionRef(ref, &ref_fullname)); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(obj_err); + if (ref_fullname.empty()) return Error("anyOf item missing $ref"); + out->push_back(std::move(ref_fullname)); + return NoError(); + }); + ECHECK(err); + return NoError(); + }; + + auto ParseFieldSchema = [&](ParsedField* out, + auto&& ParseFieldSchemaRef) -> CheckedError { + out->xfb.present = false; + out->xfb.has_type = false; + out->xfb.type = Type(); + out->xfb.has_id = false; + out->xfb.id.clear(); + out->xfb.has_presence = false; + out->xfb.presence = FieldDef::kDefault; + out->xfb.has_deprecated = false; + out->xfb.deprecated = false; + out->xfb.has_key = false; + out->xfb.key = false; + out->xfb.has_shared = false; + out->xfb.shared = false; + out->xfb.has_native_inline = false; + out->xfb.native_inline = false; + out->xfb.has_flexbuffer = false; + out->xfb.flexbuffer = false; + out->xfb.has_offset64 = false; + out->xfb.offset64 = false; + out->xfb.nested_flatbuffer.clear(); + out->xfb.union_type_field.clear(); + out->xfb.union_value_field.clear(); + out->deprecated = false; + out->description.clear(); + out->is_anyof = false; + out->anyof_types.clear(); + + std::string ref_fullname; + std::string type_name; + bool has_min = false; + bool has_max = false; + int64_t min_value = 0; + uint64_t max_value = 0; + bool has_items = false; + Type items_type; + bool has_min_items = false; + bool has_max_items = false; + int64_t min_items = 0; + int64_t max_items = 0; + + size_t fieldn = 0; + auto err = ParseTableDelimiters( + fieldn, nullptr, + [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { + if (key == "x-flatbuffers") { + ECHECK(ParseFieldXfbMeta(&out->xfb)); + } else if (key == "$ref") { + std::string ref; + ECHECK(ParseString(&ref)); + ECHECK(ExtractDefinitionRef(ref, &ref_fullname)); + } else if (key == "type") { + ECHECK(ParseString(&type_name)); + } else if (key == "minimum") { + has_min = true; + ECHECK(ParseInt64(&min_value)); + } else if (key == "maximum") { + has_max = true; + ECHECK(ParseUInt64(&max_value)); + } else if (key == "items") { + has_items = true; + ParsedField items; + items.name.clear(); + ECHECK(ParseFieldSchemaRef(&items, ParseFieldSchemaRef)); + items_type = items.type; + } else if (key == "minItems") { + has_min_items = true; + ECHECK(ParseInt64(&min_items)); + } else if (key == "maxItems") { + has_max_items = true; + ECHECK(ParseInt64(&max_items)); + } else if (key == "anyOf") { + out->is_anyof = true; + ECHECK(ParseAnyOfRefs(&out->anyof_types)); + } else if (key == "deprecated") { + ECHECK(ParseBool(&out->deprecated)); + } else if (key == "description") { + ECHECK(ParseString(&out->description)); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(err); + + if (out->xfb.present && out->xfb.has_type) { + out->type = out->xfb.type; + return NoError(); + } + + if (!ref_fullname.empty()) { + ECHECK(ResolveRefType(ref_fullname, &out->type)); + return NoError(); + } + + if (out->is_anyof) { + out->type = Type(BASE_TYPE_UNION); + return NoError(); + } + + if (type_name == "integer") { + BaseType bt = BASE_TYPE_INT; + if (has_min && has_max) InferIntegerBaseType(min_value, max_value, &bt); + out->type = Type(bt); + return NoError(); + } + if (type_name == "number") { + out->type = Type(BASE_TYPE_FLOAT); + return NoError(); + } + if (type_name == "boolean") { + out->type = Type(BASE_TYPE_BOOL); + return NoError(); + } + if (type_name == "string") { + out->type = Type(BASE_TYPE_STRING); + return NoError(); + } + if (type_name == "array") { + if (!has_items) return Error("array missing 'items'"); + const bool is_fixed = has_min_items && has_max_items && + min_items == max_items && min_items >= 0; + if (is_fixed) { + if (min_items < 1 || + min_items > flatbuffers::numeric_limits::max()) { + return Error("fixed array length out of range"); + } + out->type = + Type(BASE_TYPE_ARRAY, items_type.struct_def, items_type.enum_def, + static_cast(min_items)); + } else { + out->type = + Type(BASE_TYPE_VECTOR, items_type.struct_def, items_type.enum_def); + } + out->type.element = items_type.base_type; + return NoError(); + } + + return Error("unable to determine type"); + }; + + auto ParseRootXfbMeta = [&]() -> CheckedError { + size_t fieldn = 0; + auto err = ParseTableDelimiters( + fieldn, nullptr, + [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { + if (key == "file_identifier") { + ECHECK(ParseString(&file_identifier_)); + } else if (key == "file_extension") { + ECHECK(ParseString(&file_extension_)); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(err); + return NoError(); + }; + + auto WithNamespace = [&](const std::vector& name_space, + auto&& fn) -> CheckedError { + auto* prev_namespace = current_namespace_; + if (name_space.empty()) { + current_namespace_ = empty_namespace_; + } else { + auto* ns = new Namespace(); + ns->components = name_space; + current_namespace_ = UniqueNamespace(ns); + } + auto err = fn(); + current_namespace_ = prev_namespace; + return err; + }; + + auto CreateDefinitionsPass = [&]() -> CheckedError { + ECHECK(StartParseFile(schema_source, + schema_filename.empty() ? nullptr + : schema_filename.c_str())); + + bool found_definitions = false; + size_t fieldn = 0; + auto root_err = ParseTableDelimiters( + fieldn, nullptr, + [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { + if (key != "definitions") { + ECHECK(SkipAnyJsonValue()); + return NoError(); + } + + found_definitions = true; + size_t defn = 0; + auto defs_err = ParseTableDelimiters( + defn, nullptr, + [&](const std::string& def_fullname, size_t&, + const StructDef*) -> CheckedError { + std::string def_type; + DefinitionXfbMeta xfb; + size_t dn = 0; + auto def_err = ParseTableDelimiters( + dn, nullptr, + [&](const std::string& dk, size_t&, + const StructDef*) -> CheckedError { + if (dk == "type") { + ECHECK(ParseString(&def_type)); + } else if (dk == "x-flatbuffers") { + ECHECK(ParseDefinitionXfbMeta(&xfb)); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(def_err); + + if (def_type.empty()) { + return Error("definition missing 'type': " + def_fullname); + } + + std::string decl_name = + xfb.present && !xfb.name.empty() ? xfb.name : def_fullname; + std::vector decl_namespace = + xfb.present ? xfb.name_space : std::vector(); + + if (xfb.present && (!xfb.name.empty() || !xfb.name_space.empty())) { + const auto expected = MakeSchemaFullName(decl_namespace, decl_name); + if (expected != def_fullname) { + return Error("definition name mismatch: '" + def_fullname + + "' vs '" + expected + "'"); + } + } + + return WithNamespace(decl_namespace, [&]() -> CheckedError { + if (def_type == "string") { + const bool is_union = xfb.present && xfb.kind == "union"; + EnumDef* enum_def = nullptr; + ECHECK(StartEnum(decl_name, is_union, &enum_def)); + enum_by_fullname[def_fullname] = enum_def; + } else if (def_type == "object") { + StructDef* struct_def = nullptr; + ECHECK(StartStruct(decl_name, &struct_def)); + if (xfb.present) { + if (xfb.kind == "struct") struct_def->fixed = true; + if (xfb.kind == "table") struct_def->fixed = false; + if (xfb.has_key) struct_def->has_key = true; + } else { + struct_def->fixed = false; + } + struct_def->sortbysize = !struct_def->fixed; + struct_by_fullname[def_fullname] = struct_def; + } else { + return Error("unsupported definition type: " + def_type); + } + return NoError(); + }); + }); + ECHECK(defs_err); + return NoError(); + }); + ECHECK(root_err); + + if (opts.require_json_eof) EXPECT(kTokenEof); + if (!found_definitions) return Error("JSON Schema missing 'definitions'"); + return NoError(); + }; + + ECHECK(CreateDefinitionsPass()); + + ECHECK(StartParseFile(schema_source, + schema_filename.empty() ? nullptr + : schema_filename.c_str())); + + std::string root_ref_fullname; + + auto ApplyEnumMeta = [&](EnumDef& enum_def, + const DefinitionXfbMeta& xfb) -> CheckedError { + if (!xfb.present) return NoError(); + + if (!xfb.kind.empty()) { + enum_def.is_union = xfb.kind == "union"; + if (enum_def.is_union) { + enum_def.underlying_type.base_type = BASE_TYPE_UTYPE; + enum_def.underlying_type.enum_def = &enum_def; + } + } + + if (!xfb.underlying_type.empty()) { + BaseType bt = BASE_TYPE_NONE; + if (!BaseTypeFromScalarName(xfb.underlying_type, &bt)) { + return Error("invalid enum underlying_type: " + xfb.underlying_type); + } + if (enum_def.is_union) { + if (bt != BASE_TYPE_UTYPE) { + return Error("union underlying_type must be utype"); + } + } else if (!IsInteger(bt)) { + return Error("invalid enum underlying_type: " + xfb.underlying_type); + } + enum_def.underlying_type.base_type = bt; + enum_def.underlying_type.enum_def = &enum_def; + } + return NoError(); + }; + + auto FillEnum = [&](EnumDef& enum_def, + const DefinitionXfbMeta& xfb, + const std::vector& enum_values) + -> CheckedError { + if (enum_def.size()) return Error("enum redefinition: " + enum_def.name); + + ECHECK(ApplyEnumMeta(enum_def, xfb)); + + EnumValBuilder evb(*this, enum_def); + + if (xfb.present && !xfb.values.empty()) { + for (const auto& vm : xfb.values) { + int64_t v = 0; + if (enum_def.underlying_type.base_type == BASE_TYPE_ULONG) { + uint64_t u = 0; + if (!StringToNumber(vm.value.c_str(), &u)) { + return Error("invalid enum value: " + vm.value); + } + v = static_cast(u); + } else { + if (!StringToNumber(vm.value.c_str(), &v)) { + return Error("invalid enum value: " + vm.value); + } + } + + auto* ev = evb.CreateEnumerator(vm.name, v); + if (enum_def.is_union && !vm.union_type.empty()) { + auto it = struct_by_fullname.find(vm.union_type); + if (it == struct_by_fullname.end()) { + return Error("unknown union_type: " + vm.union_type); + } + ev->union_type = Type(BASE_TYPE_STRUCT, it->second); + } + ECHECK(evb.AcceptEnumerator(vm.name)); + } + } else { + for (const auto& name : enum_values) { + evb.CreateEnumerator(name); + ECHECK(evb.AcceptEnumerator(name)); + } + } + + enum_def.SortByValue(); + return NoError(); + }; + + auto FillStruct = [&](StructDef& struct_def, + const DefinitionXfbMeta& xfb, + const std::vector& fields, + const std::vector& required_fields, + const std::string& description) -> CheckedError { + if (!struct_def.fields.vec.empty()) { + return Error("struct redefinition: " + struct_def.name); + } + + if (!description.empty()) struct_def.doc_comment = SplitLines(description); + + if (xfb.present && !xfb.kind.empty()) { + if (xfb.kind == "struct") struct_def.fixed = true; + if (xfb.kind == "table") struct_def.fixed = false; + if (xfb.has_key) struct_def.has_key = true; + } else { + for (const auto& field : fields) { + if (field.type.base_type == BASE_TYPE_ARRAY) { + struct_def.fixed = true; + break; + } + } + } + struct_def.sortbysize = !struct_def.fixed; + + std::map field_by_name; + std::map explicit_union_type_field_by_value; + std::vector>> pending_unions; + + for (const auto& parsed : fields) { + FieldDef* field_def = nullptr; + ECHECK(AddField(struct_def, parsed.name, parsed.type, &field_def)); + field_by_name[parsed.name] = field_def; + + if (parsed.deprecated) field_def->deprecated = true; + if (!parsed.description.empty()) { + field_def->doc_comment = SplitLines(parsed.description); + } + + ECHECK(ApplyFieldXfb(*field_def, parsed.xfb)); + if (parsed.xfb.present && parsed.xfb.has_deprecated) { + field_def->deprecated = parsed.xfb.deprecated; + } + + if (!parsed.xfb.union_type_field.empty()) { + explicit_union_type_field_by_value[parsed.name] = parsed.xfb.union_type_field; + } + + if (parsed.is_anyof) { + pending_unions.emplace_back(field_def, parsed.anyof_types); + } + } + + // Apply required list. + for (const auto& req : required_fields) { + auto it = field_by_name.find(req); + if (it == field_by_name.end()) { + return Error("unknown required field: " + req); + } + it->second->presence = FieldDef::kRequired; + } + + // Resolve unions (value field + type field + union enum mapping). + for (auto& pending : pending_unions) { + FieldDef* value_field = pending.first; + const auto& anyof_types = pending.second; + + const auto explicit_it = + explicit_union_type_field_by_value.find(value_field->name); + const std::string type_field_name = + explicit_it != explicit_union_type_field_by_value.end() + ? explicit_it->second + : value_field->name + UnionTypeFieldSuffix(); + + auto it_type_field = field_by_name.find(type_field_name); + if (it_type_field == field_by_name.end()) continue; + FieldDef* type_field = it_type_field->second; + + auto* enum_def = type_field->value.type.enum_def; + if (!enum_def) { + enum_def = value_field->value.type.enum_def; + } + if (!enum_def) { + return Error("unable to resolve union enum for field: " + + value_field->name); + } + + enum_def->is_union = true; + enum_def->underlying_type.base_type = BASE_TYPE_UTYPE; + enum_def->underlying_type.enum_def = enum_def; + + value_field->value.type.base_type = BASE_TYPE_UNION; + value_field->value.type.enum_def = enum_def; + + type_field->value.type.base_type = BASE_TYPE_UTYPE; + type_field->value.type.enum_def = enum_def; + + value_field->sibling_union_field = type_field; + type_field->sibling_union_field = value_field; + + // Fill union value -> type mapping if not already present. + if (enum_def->Vals().empty()) continue; + + const bool has_none = enum_def->Vals().front()->name == "NONE"; + const size_t offset = has_none ? 1 : 0; + if (enum_def->Vals().size() < offset) continue; + if (enum_def->Vals().size() - offset != anyof_types.size()) { + return Error("union anyOf size mismatch for " + enum_def->name); + } + + bool already_mapped = false; + for (size_t i = offset; i < enum_def->Vals().size(); ++i) { + if (enum_def->Vals()[i]->union_type.base_type != BASE_TYPE_NONE) { + already_mapped = true; + break; + } + } + + for (size_t i = 0; i < anyof_types.size(); ++i) { + auto sit = struct_by_fullname.find(anyof_types[i]); + if (sit == struct_by_fullname.end()) { + return Error("unknown union anyOf type: " + anyof_types[i]); + } + if (already_mapped) { + continue; + } + enum_def->Vals()[i + offset]->union_type = + Type(BASE_TYPE_STRUCT, sit->second); + } + } + + if (struct_def.fixed) struct_def.PadLastField(struct_def.minalign); + return NoError(); + }; + + size_t root_fieldn = 0; + auto root_err = ParseTableDelimiters( + root_fieldn, nullptr, + [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { + if (key == "$ref") { + std::string ref; + ECHECK(ParseString(&ref)); + ECHECK(ExtractDefinitionRef(ref, &root_ref_fullname)); + return NoError(); + } + if (key == "x-flatbuffers") { + ECHECK(ParseRootXfbMeta()); + return NoError(); + } + if (key != "definitions") { + ECHECK(SkipAnyJsonValue()); + return NoError(); + } + + size_t defn = 0; + auto defs_err = ParseTableDelimiters( + defn, nullptr, + [&](const std::string& def_fullname, size_t&, + const StructDef*) -> CheckedError { + auto eit = enum_by_fullname.find(def_fullname); + auto sit = struct_by_fullname.find(def_fullname); + if (eit == enum_by_fullname.end() && + sit == struct_by_fullname.end()) { + return Error("unknown definition: " + def_fullname); + } + + std::string def_type; + std::string description; + DefinitionXfbMeta xfb; + std::vector enum_values; + std::vector required_fields; + std::vector fields; + + size_t dn = 0; + auto def_err = ParseTableDelimiters( + dn, nullptr, + [&](const std::string& dk, size_t&, const StructDef*) + -> CheckedError { + if (dk == "type") { + ECHECK(ParseString(&def_type)); + } else if (dk == "description") { + ECHECK(ParseString(&description)); + } else if (dk == "x-flatbuffers") { + ECHECK(ParseDefinitionXfbMeta(&xfb)); + } else if (dk == "enum") { + ECHECK(ParseStringArray(&enum_values)); + } else if (dk == "required") { + ECHECK(ParseStringArray(&required_fields)); + } else if (dk == "properties") { + size_t pn = 0; + auto props_err = ParseTableDelimiters( + pn, nullptr, + [&](const std::string& field_name, size_t&, + const StructDef*) -> CheckedError { + ParsedField parsed; + parsed.name = field_name; + ECHECK(ParseFieldSchema(&parsed, ParseFieldSchema)); + fields.push_back(std::move(parsed)); + return NoError(); + }); + ECHECK(props_err); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(def_err); + + if (eit != enum_by_fullname.end()) { + if (!def_type.empty() && def_type != "string") { + return Error("enum type must be 'string': " + def_fullname); + } + if (!description.empty()) { + eit->second->doc_comment = SplitLines(description); + } + ECHECK(FillEnum(*eit->second, xfb, enum_values)); + } else { + if (!def_type.empty() && def_type != "object") { + return Error("struct type must be 'object': " + def_fullname); + } + ECHECK(FillStruct(*sit->second, xfb, fields, required_fields, + description)); + } + + return NoError(); + }); + ECHECK(defs_err); + return NoError(); + }); + ECHECK(root_err); + + if (opts.require_json_eof) EXPECT(kTokenEof); + + if (root_ref_fullname.empty()) return Error("JSON Schema missing root '$ref'"); + auto it_root = struct_by_fullname.find(root_ref_fullname); + if (it_root == struct_by_fullname.end()) { + return Error("root '$ref' is not a struct definition: " + root_ref_fullname); + } + root_struct_def_ = it_root->second; + + return NoError(); +} + std::set Parser::GetIncludedFilesRecursive( const std::string& file_name) const { std::set included_files; diff --git a/tests/JsonSchemaTest.sh b/tests/JsonSchemaTest.sh index fe11f00ab72..77dfbae0db2 100755 --- a/tests/JsonSchemaTest.sh +++ b/tests/JsonSchemaTest.sh @@ -83,6 +83,19 @@ PY done } +compare_output_against_dir() { + local expected_dir="$1" + local out_dir="$2" + for golden in "${golden_files[@]}"; do + local expected="${expected_dir}/${golden}" + local generated="${out_dir}/${golden}" + if ! diff -u "${expected}" "${generated}"; then + echo "JSON Schema mismatch for ${golden}" >&2 + exit 1 + fi + done +} + run_case_diff() { local label="$1" local out_dir="$2" @@ -107,12 +120,45 @@ run_case_xflatbuffers() { compare_output_stripping_xflatbuffers "${out_dir}" } +run_case_roundtrip_golden() { + local label="$1" + local out_dir="$2" + shift 2 + + echo "Round-tripping JSON Schemas (${label})" + rm -rf "${out_dir}" + mkdir -p "${out_dir}" + ( cd "${script_dir}" && "${flatc}" "$@" -o "${out_dir}" "${golden_files[@]}" ) + compare_output "${out_dir}" +} + +run_case_roundtrip_xflatbuffers() { + local label="$1" + local gen_dir="$2" + local out_dir="$3" + shift 3 + + echo "Round-tripping JSON Schemas (${label})" + rm -rf "${gen_dir}" "${out_dir}" + mkdir -p "${gen_dir}" "${out_dir}" + + ( cd "${script_dir}" && "${flatc}" "$@" "${include_flags[@]}" -o "${gen_dir}" "${schemas[@]}" ) + ( cd "${gen_dir}" && "${flatc}" "$@" -o "${out_dir}" "${golden_files[@]}" ) + + compare_output_against_dir "${gen_dir}" "${out_dir}" +} + tmp_default="$(mktemp -d)" tmp_preserve="$(mktemp -d)" tmp_xflatbuffers_default="$(mktemp -d)" tmp_xflatbuffers_preserve="$(mktemp -d)" +tmp_roundtrip_default="$(mktemp -d)" +tmp_roundtrip_xfb_gen="$(mktemp -d)" +tmp_roundtrip_xfb="$(mktemp -d)" cleanup() { - rm -rf "${tmp_default}" "${tmp_preserve}" "${tmp_xflatbuffers_default}" "${tmp_xflatbuffers_preserve}" + rm -rf "${tmp_default}" "${tmp_preserve}" \ + "${tmp_xflatbuffers_default}" "${tmp_xflatbuffers_preserve}" \ + "${tmp_roundtrip_default}" "${tmp_roundtrip_xfb_gen}" "${tmp_roundtrip_xfb}" } trap cleanup EXIT @@ -120,5 +166,7 @@ run_case_diff "default naming" "${tmp_default}" --jsonschema run_case_diff "preserve-case naming" "${tmp_preserve}" --jsonschema --preserve-case run_case_xflatbuffers "x-flatbuffers metadata" "${tmp_xflatbuffers_default}" --jsonschema --jsonschema-xflatbuffers run_case_xflatbuffers "x-flatbuffers metadata + preserve-case" "${tmp_xflatbuffers_preserve}" --jsonschema --jsonschema-xflatbuffers --preserve-case +run_case_roundtrip_golden "goldens" "${tmp_roundtrip_default}" --jsonschema +run_case_roundtrip_xflatbuffers "x-flatbuffers metadata" "${tmp_roundtrip_xfb_gen}" "${tmp_roundtrip_xfb}" --jsonschema --jsonschema-xflatbuffers -echo "JSON Schema tests (default + preserve-case + x-flatbuffers) passed" +echo "JSON Schema tests (generation + roundtrip) passed" From 92eb41fee2ef8fb90b3921613a5a6dcb78e798f5 Mon Sep 17 00:00:00 2001 From: TJKoury Date: Thu, 18 Dec 2025 12:35:12 -0500 Subject: [PATCH 3/5] updates to docs and metaschema --- README.md | 2 + docs/mkdocs.yml | 1 + docs/source/flatc.md | 6 +- docs/source/json_schema.md | 48 +++ docs/source/schemas/x-flatbuffers.schema.json | 223 ++++++++++++++ tests/JsonSchemaTest.sh | 274 ++++++++++++++++++ 6 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 docs/source/json_schema.md create mode 100644 docs/source/schemas/x-flatbuffers.schema.json diff --git a/README.md b/README.md index a7463c5c9af..19a9bc580d7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ **This is a fork of the Google Flatbuffers Library with the following features added:** - A `--preserve-case` flag to prevent IDL name mangling +- JSON Schema schema import/export (`--jsonschema`, `*.schema.json`) +- Optional lossless JSON Schema round-tripping via `--jsonschema-xflatbuffers` metadata ![logo](https://flatbuffers.dev/assets/flatbuffers_logo.svg) FlatBuffers diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index cbcbb48de28..c3e7b379c99 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -128,6 +128,7 @@ nav: - Compiler (flatc): - Building: "building.md" - Using: "flatc.md" + - JSON Schema: "json_schema.md" - Schema (.fbs): - Overview: "schema.md" - Evolution: "evolution.md" diff --git a/docs/source/flatc.md b/docs/source/flatc.md index 330ebc3d9a6..4e2c5fa0018 100644 --- a/docs/source/flatc.md +++ b/docs/source/flatc.md @@ -51,6 +51,11 @@ Additionally, adding: * `--grpc` Will generate RPC stub code for gRPC (not available in all languages) +Other schema outputs: + + * `--jsonschema`: Generate JSON Schema (`*.schema.json`) from a FlatBuffers schema. + * `--jsonschema-xflatbuffers`: Include `x-flatbuffers` vendor metadata in the generated JSON Schema for lossless round-tripping. See [JSON Schema](json_schema.md). + ### Data Files If `FILES...` contain data files, they can be exported to either a binary or @@ -305,4 +310,3 @@ Additional gRPC options: NOTE: short-form options for generators are deprecated, use the long form whenever possible. - diff --git a/docs/source/json_schema.md b/docs/source/json_schema.md new file mode 100644 index 00000000000..0e705a61667 --- /dev/null +++ b/docs/source/json_schema.md @@ -0,0 +1,48 @@ +# JSON Schema + +`flatc` can generate [JSON Schema](https://json-schema.org/) for a FlatBuffers schema, and it can also import that JSON Schema back into FlatBuffers' schema IR. + +## Generate JSON Schema + +Generate `*.schema.json` from `*.fbs`: + +```sh +flatc --jsonschema -o out_dir schema.fbs +``` + +This produces `out_dir/schema.schema.json`. + +## Import JSON Schema + +You can use a `*.schema.json` file anywhere `flatc` expects a schema file: + +```sh +flatc --cpp -o out_dir schema.schema.json +``` + +This is primarily intended for round-tripping FlatBuffers schemas through JSON Schema tooling, and for downstream workflows that treat JSON Schema as the schema “source of truth”. + +## `x-flatbuffers` metadata (optional) + +JSON Schema cannot represent all FlatBuffers schema semantics (for example: struct vs table, exact scalar widths, union type mapping, field ids, and presence rules). To enable lossless round-trips, `flatc` can emit an optional vendor extension: + +```sh +flatc --jsonschema --jsonschema-xflatbuffers -o out_dir schema.fbs +``` + +This adds `x-flatbuffers` objects at: + +- The schema root (`root_type`, `file_identifier`, `file_extension`) +- Each definition (enum/union/table/struct metadata) +- Each field (exact FlatBuffers type + attributes) + +Because `x-flatbuffers` uses the standard vendor-extension mechanism (`x-...`), most JSON Schema tools will ignore it and continue to work normally (for example QuickType and similar code generators). + +### `x-flatbuffers` meta-schema + +The allowed keys/values for the `x-flatbuffers` objects are described by: + +- `docs/source/schemas/x-flatbuffers.schema.json:1` + +This file is a meta-schema for the `x-flatbuffers` vendor extension itself (not a replacement for the JSON Schema metaschema). + diff --git a/docs/source/schemas/x-flatbuffers.schema.json b/docs/source/schemas/x-flatbuffers.schema.json new file mode 100644 index 00000000000..6f94df4b1ff --- /dev/null +++ b/docs/source/schemas/x-flatbuffers.schema.json @@ -0,0 +1,223 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "FlatBuffers x-flatbuffers Vendor Extension", + "description": "A meta-schema for the `x-flatbuffers` vendor extension emitted by `flatc --jsonschema --jsonschema-xflatbuffers`.", + "oneOf": [ + { "$ref": "#/$defs/root" }, + { "$ref": "#/$defs/enum_def" }, + { "$ref": "#/$defs/struct_def" }, + { "$ref": "#/$defs/field" }, + { "$ref": "#/$defs/type" } + ], + "$defs": { + "namespace": { + "type": "array", + "items": { "type": "string" } + }, + "presence": { + "type": "string", + "enum": ["required", "optional", "default"] + }, + "scalar_name": { + "type": "string", + "enum": [ + "bool", + "byte", + "ubyte", + "short", + "ushort", + "int", + "uint", + "long", + "ulong", + "float", + "double", + "string", + "utype" + ] + }, + "enum_underlying_type": { + "type": "string", + "enum": [ + "byte", + "ubyte", + "short", + "ushort", + "int", + "uint", + "long", + "ulong", + "utype" + ] + }, + "type": { + "type": "object", + "properties": { + "base": { + "type": "string", + "enum": [ + "bool", + "byte", + "ubyte", + "short", + "ushort", + "int", + "uint", + "long", + "ulong", + "float", + "double", + "string", + "utype", + "struct", + "table", + "union", + "enum", + "vector", + "array" + ] + }, + "ref": { "type": "string" }, + "scalar": { "$ref": "#/$defs/scalar_name" }, + "vector64": { "type": "boolean" }, + "fixed_length": { "type": "integer", "minimum": 0 }, + "element": { "$ref": "#/$defs/type" } + }, + "required": ["base"], + "additionalProperties": false, + "oneOf": [ + { + "properties": { "base": { "enum": ["bool", "byte", "ubyte", "short", "ushort", "int", "uint", "long", "ulong", "float", "double", "string"] } }, + "required": ["base"], + "additionalProperties": false + }, + { + "properties": { "base": { "const": "utype" }, "ref": { "type": "string" } }, + "required": ["base", "ref"], + "additionalProperties": false + }, + { + "properties": { "base": { "enum": ["struct", "table"] }, "ref": { "type": "string" } }, + "required": ["base", "ref"], + "additionalProperties": false + }, + { + "properties": { "base": { "const": "union" }, "ref": { "type": "string" } }, + "required": ["base", "ref"], + "additionalProperties": false + }, + { + "properties": { "base": { "const": "enum" }, "ref": { "type": "string" }, "scalar": { "$ref": "#/$defs/scalar_name" } }, + "required": ["base", "ref", "scalar"], + "additionalProperties": false + }, + { + "properties": { "base": { "const": "vector" }, "element": { "$ref": "#/$defs/type" }, "vector64": { "type": "boolean" } }, + "required": ["base", "element"], + "additionalProperties": false + }, + { + "properties": { "base": { "const": "array" }, "element": { "$ref": "#/$defs/type" }, "fixed_length": { "type": "integer", "minimum": 0 } }, + "required": ["base", "element", "fixed_length"], + "additionalProperties": false + } + ] + }, + "field": { + "type": "object", + "properties": { + "type": { "$ref": "#/$defs/type" }, + "id": { + "oneOf": [ + { "type": "integer" }, + { "type": "string" } + ] + }, + "presence": { "$ref": "#/$defs/presence" }, + "deprecated": { "type": "boolean" }, + "key": { "type": "boolean" }, + "shared": { "type": "boolean" }, + "native_inline": { "type": "boolean" }, + "flexbuffer": { "type": "boolean" }, + "offset64": { "type": "boolean" }, + "nested_flatbuffer": { "type": "string" }, + "union_type_field": { "type": "string" }, + "union_value_field": { "type": "string" } + }, + "required": ["type", "presence"], + "additionalProperties": false + }, + "enum_value": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": { "type": "string" }, + "union_type": { "type": "string" } + }, + "required": ["name", "value"], + "additionalProperties": false + }, + "enum_def": { + "type": "object", + "properties": { + "kind": { "type": "string", "enum": ["enum", "union"] }, + "name": { "type": "string" }, + "namespace": { "$ref": "#/$defs/namespace" }, + "underlying_type": { "$ref": "#/$defs/enum_underlying_type" }, + "values": { + "type": "array", + "items": { "$ref": "#/$defs/enum_value" } + } + }, + "required": ["kind", "name", "namespace", "underlying_type", "values"], + "additionalProperties": false, + "allOf": [ + { + "if": { "properties": { "kind": { "const": "union" } }, "required": ["kind"] }, + "then": { "properties": { "underlying_type": { "const": "utype" } } } + } + ] + }, + "struct_def": { + "type": "object", + "properties": { + "kind": { "type": "string", "enum": ["struct", "table"] }, + "name": { "type": "string" }, + "namespace": { "$ref": "#/$defs/namespace" }, + "has_key": { "type": "boolean" }, + "minalign": { "type": "integer", "minimum": 1 }, + "bytesize": { "type": "integer", "minimum": 0 } + }, + "required": ["kind", "name", "namespace"], + "additionalProperties": false, + "allOf": [ + { + "if": { "properties": { "kind": { "const": "struct" } }, "required": ["kind"] }, + "then": { "required": ["minalign", "bytesize"] } + }, + { + "if": { "properties": { "kind": { "const": "table" } }, "required": ["kind"] }, + "then": { + "not": { + "anyOf": [ + { "required": ["minalign"] }, + { "required": ["bytesize"] } + ] + } + } + } + ] + }, + "root": { + "type": "object", + "properties": { + "root_type": { "type": "string" }, + "file_identifier": { "type": "string", "minLength": 4, "maxLength": 4 }, + "file_extension": { "type": "string" } + }, + "required": ["root_type"], + "additionalProperties": false + } + } +} + diff --git a/tests/JsonSchemaTest.sh b/tests/JsonSchemaTest.sh index 77dfbae0db2..9297f18d097 100755 --- a/tests/JsonSchemaTest.sh +++ b/tests/JsonSchemaTest.sh @@ -27,6 +27,277 @@ golden_files=( "arrays_test.schema.json" ) +xfb_metaschema="${repo_dir}/docs/source/schemas/x-flatbuffers.schema.json" +if [[ ! -f "${xfb_metaschema}" ]]; then + echo "Missing x-flatbuffers meta-schema at ${xfb_metaschema}" >&2 + exit 1 +fi + +validate_xflatbuffers_metadata_file() { + local generated_path="$1" + python3 - "${generated_path}" "${xfb_metaschema}" <<'PY' +import json +import sys + + +schema_path, metaschema_path = sys.argv[1], sys.argv[2] + +# Ensure the meta-schema is parseable JSON (tooling can consume this file). +with open(metaschema_path, "r", encoding="utf-8") as f: + json.load(f) + +with open(schema_path, "r", encoding="utf-8") as f: + schema = json.load(f) + + +SCALAR_NAMES = { + "bool", + "byte", + "ubyte", + "short", + "ushort", + "int", + "uint", + "long", + "ulong", + "float", + "double", + "string", + "utype", +} + +TYPE_BASE_NAMES = SCALAR_NAMES | {"struct", "table", "union", "enum", "vector", "array"} + +PRESENCE = {"required", "optional", "default"} + +ENUM_UNDERLYING_TYPES = {"byte", "ubyte", "short", "ushort", "int", "uint", "long", "ulong", "utype"} + + +def path_str(path) -> str: + parts = [] + for p in path: + if isinstance(p, int): + parts.append(f"[{p}]") + else: + parts.append(str(p)) + return ".".join(parts) + + +def fail(path, msg): + raise AssertionError(f"{schema_path}:{path_str(path)}: {msg}") + + +def ensure(cond, path, msg): + if not cond: + fail(path, msg) + + +def ensure_type(obj, t, path, msg): + if not isinstance(obj, t): + fail(path, msg) + + +def validate_type_meta(obj, path): + ensure_type(obj, dict, path, "x-flatbuffers.type must be an object") + allowed = {"base", "ref", "scalar", "vector64", "fixed_length", "element"} + unknown = set(obj.keys()) - allowed + ensure(not unknown, path, f"unknown keys in x-flatbuffers type: {sorted(unknown)}") + + base = obj.get("base") + ensure(isinstance(base, str), path, "x-flatbuffers.type.base must be a string") + ensure(base in TYPE_BASE_NAMES, path, f"invalid x-flatbuffers.type.base: {base!r}") + + if base in {"bool", "byte", "ubyte", "short", "ushort", "int", "uint", "long", "ulong", "float", "double", "string"}: + ensure(set(obj.keys()) == {"base"}, path, f"scalar x-flatbuffers.type must only contain 'base' (got {sorted(obj.keys())})") + return + + if base in {"struct", "table", "union", "utype"}: + ensure("ref" in obj, path, f"x-flatbuffers.type for base={base!r} requires 'ref'") + ensure_type(obj["ref"], str, path + ["ref"], "x-flatbuffers.type.ref must be a string") + ensure(set(obj.keys()) == {"base", "ref"}, path, f"x-flatbuffers.type for base={base!r} must only contain 'base' and 'ref'") + return + + if base == "enum": + ensure("ref" in obj, path, "x-flatbuffers.type for base='enum' requires 'ref'") + ensure("scalar" in obj, path, "x-flatbuffers.type for base='enum' requires 'scalar'") + ensure_type(obj["ref"], str, path + ["ref"], "x-flatbuffers.type.ref must be a string") + ensure_type(obj["scalar"], str, path + ["scalar"], "x-flatbuffers.type.scalar must be a string") + ensure(obj["scalar"] in SCALAR_NAMES, path + ["scalar"], f"invalid x-flatbuffers.type.scalar: {obj['scalar']!r}") + ensure(set(obj.keys()) == {"base", "ref", "scalar"}, path, "x-flatbuffers.type for base='enum' must only contain 'base', 'ref', and 'scalar'") + return + + if base == "vector": + ensure("element" in obj, path, "x-flatbuffers.type for base='vector' requires 'element'") + validate_type_meta(obj["element"], path + ["element"]) + if "vector64" in obj: + ensure_type(obj["vector64"], bool, path + ["vector64"], "x-flatbuffers.type.vector64 must be boolean") + allowed_keys = {"base", "element", "vector64"} if "vector64" in obj else {"base", "element"} + ensure(set(obj.keys()) == allowed_keys, path, f"x-flatbuffers.type for base='vector' has unexpected keys: {sorted(set(obj.keys()) - allowed_keys)}") + return + + if base == "array": + ensure("element" in obj, path, "x-flatbuffers.type for base='array' requires 'element'") + ensure("fixed_length" in obj, path, "x-flatbuffers.type for base='array' requires 'fixed_length'") + validate_type_meta(obj["element"], path + ["element"]) + ensure_type(obj["fixed_length"], int, path + ["fixed_length"], "x-flatbuffers.type.fixed_length must be integer") + ensure(obj["fixed_length"] >= 0, path + ["fixed_length"], "x-flatbuffers.type.fixed_length must be >= 0") + ensure(set(obj.keys()) == {"base", "element", "fixed_length"}, path, "x-flatbuffers.type for base='array' must only contain 'base', 'element', and 'fixed_length'") + return + + fail(path, f"unhandled x-flatbuffers.type.base: {base!r}") + + +def validate_field_meta(obj, path): + ensure_type(obj, dict, path, "x-flatbuffers field metadata must be an object") + allowed = { + "type", + "id", + "presence", + "deprecated", + "key", + "shared", + "native_inline", + "flexbuffer", + "offset64", + "nested_flatbuffer", + "union_type_field", + "union_value_field", + } + unknown = set(obj.keys()) - allowed + ensure(not unknown, path, f"unknown keys in x-flatbuffers field metadata: {sorted(unknown)}") + + ensure("type" in obj, path, "x-flatbuffers field metadata missing 'type'") + ensure("presence" in obj, path, "x-flatbuffers field metadata missing 'presence'") + validate_type_meta(obj["type"], path + ["type"]) + ensure_type(obj["presence"], str, path + ["presence"], "x-flatbuffers.presence must be a string") + ensure(obj["presence"] in PRESENCE, path + ["presence"], f"invalid x-flatbuffers.presence: {obj['presence']!r}") + + if "id" in obj: + ensure(isinstance(obj["id"], (int, str)), path + ["id"], "x-flatbuffers.id must be integer or string") + for key in ("deprecated", "key", "shared", "native_inline", "flexbuffer", "offset64"): + if key in obj: + ensure_type(obj[key], bool, path + [key], f"x-flatbuffers.{key} must be boolean") + for key in ("nested_flatbuffer", "union_type_field", "union_value_field"): + if key in obj: + ensure_type(obj[key], str, path + [key], f"x-flatbuffers.{key} must be a string") + + +def validate_enum_def_meta(obj, path): + ensure_type(obj, dict, path, "x-flatbuffers enum metadata must be an object") + allowed = {"kind", "name", "namespace", "underlying_type", "values"} + unknown = set(obj.keys()) - allowed + ensure(not unknown, path, f"unknown keys in x-flatbuffers enum metadata: {sorted(unknown)}") + + for req in ("kind", "name", "namespace", "underlying_type", "values"): + ensure(req in obj, path, f"x-flatbuffers enum metadata missing {req!r}") + + ensure_type(obj["kind"], str, path + ["kind"], "x-flatbuffers.kind must be a string") + ensure(obj["kind"] in {"enum", "union"}, path + ["kind"], f"invalid x-flatbuffers.kind for enum: {obj['kind']!r}") + ensure_type(obj["name"], str, path + ["name"], "x-flatbuffers.name must be a string") + ensure_type(obj["namespace"], list, path + ["namespace"], "x-flatbuffers.namespace must be an array") + for i, v in enumerate(obj["namespace"]): + ensure_type(v, str, path + ["namespace", i], "x-flatbuffers.namespace items must be strings") + ensure_type(obj["underlying_type"], str, path + ["underlying_type"], "x-flatbuffers.underlying_type must be a string") + ensure(obj["underlying_type"] in ENUM_UNDERLYING_TYPES, path + ["underlying_type"], f"invalid x-flatbuffers.underlying_type: {obj['underlying_type']!r}") + if obj["kind"] == "union": + ensure(obj["underlying_type"] == "utype", path + ["underlying_type"], "union underlying_type must be 'utype'") + + ensure_type(obj["values"], list, path + ["values"], "x-flatbuffers.values must be an array") + for i, val in enumerate(obj["values"]): + ensure_type(val, dict, path + ["values", i], "x-flatbuffers.values items must be objects") + allowed_val = {"name", "value", "union_type"} + unknown_val = set(val.keys()) - allowed_val + ensure(not unknown_val, path + ["values", i], f"unknown keys in enum value metadata: {sorted(unknown_val)}") + ensure("name" in val, path + ["values", i], "enum value metadata missing 'name'") + ensure("value" in val, path + ["values", i], "enum value metadata missing 'value'") + ensure_type(val["name"], str, path + ["values", i, "name"], "enum value name must be a string") + ensure_type(val["value"], str, path + ["values", i, "value"], "enum value must be a string") + if "union_type" in val: + ensure_type(val["union_type"], str, path + ["values", i, "union_type"], "enum value union_type must be a string") + + +def validate_struct_def_meta(obj, path): + ensure_type(obj, dict, path, "x-flatbuffers struct/table metadata must be an object") + allowed = {"kind", "name", "namespace", "has_key", "minalign", "bytesize"} + unknown = set(obj.keys()) - allowed + ensure(not unknown, path, f"unknown keys in x-flatbuffers struct/table metadata: {sorted(unknown)}") + + for req in ("kind", "name", "namespace"): + ensure(req in obj, path, f"x-flatbuffers struct/table metadata missing {req!r}") + + ensure_type(obj["kind"], str, path + ["kind"], "x-flatbuffers.kind must be a string") + ensure(obj["kind"] in {"struct", "table"}, path + ["kind"], f"invalid x-flatbuffers.kind for struct/table: {obj['kind']!r}") + ensure_type(obj["name"], str, path + ["name"], "x-flatbuffers.name must be a string") + ensure_type(obj["namespace"], list, path + ["namespace"], "x-flatbuffers.namespace must be an array") + for i, v in enumerate(obj["namespace"]): + ensure_type(v, str, path + ["namespace", i], "x-flatbuffers.namespace items must be strings") + if "has_key" in obj: + ensure_type(obj["has_key"], bool, path + ["has_key"], "x-flatbuffers.has_key must be boolean") + if obj["kind"] == "struct": + ensure("minalign" in obj and "bytesize" in obj, path, "struct metadata must include 'minalign' and 'bytesize'") + ensure_type(obj["minalign"], int, path + ["minalign"], "x-flatbuffers.minalign must be integer") + ensure_type(obj["bytesize"], int, path + ["bytesize"], "x-flatbuffers.bytesize must be integer") + else: + ensure("minalign" not in obj and "bytesize" not in obj, path, "table metadata must not include 'minalign' or 'bytesize'") + + +def validate_root_meta(obj, path): + ensure_type(obj, dict, path, "x-flatbuffers root metadata must be an object") + allowed = {"root_type", "file_identifier", "file_extension"} + unknown = set(obj.keys()) - allowed + ensure(not unknown, path, f"unknown keys in x-flatbuffers root metadata: {sorted(unknown)}") + + ensure("root_type" in obj, path, "x-flatbuffers root metadata missing 'root_type'") + ensure_type(obj["root_type"], str, path + ["root_type"], "x-flatbuffers.root_type must be a string") + if "file_identifier" in obj: + ensure_type(obj["file_identifier"], str, path + ["file_identifier"], "x-flatbuffers.file_identifier must be a string") + ensure(len(obj["file_identifier"]) == 4, path + ["file_identifier"], "x-flatbuffers.file_identifier must be 4 characters") + if "file_extension" in obj: + ensure_type(obj["file_extension"], str, path + ["file_extension"], "x-flatbuffers.file_extension must be a string") + + +def validate_xflatbuffers(obj, path): + # Prefer structural classification (so this works even if metadata moves). + if isinstance(obj, dict) and "root_type" in obj: + validate_root_meta(obj, path) + return + if isinstance(obj, dict) and "type" in obj and "presence" in obj: + validate_field_meta(obj, path) + return + if isinstance(obj, dict) and obj.get("kind") in {"enum", "union"}: + validate_enum_def_meta(obj, path) + return + if isinstance(obj, dict) and obj.get("kind") in {"struct", "table"}: + validate_struct_def_meta(obj, path) + return + if isinstance(obj, dict) and "base" in obj: + validate_type_meta(obj, path) + return + fail(path, "unrecognized x-flatbuffers metadata object") + + +def walk(obj, path): + if isinstance(obj, dict): + if "x-flatbuffers" in obj: + validate_xflatbuffers(obj["x-flatbuffers"], path + ["x-flatbuffers"]) + for k, v in obj.items(): + walk(v, path + [k]) + elif isinstance(obj, list): + for i, v in enumerate(obj): + walk(v, path + [i]) + + +walk(schema, []) +PY +} + +validate_xflatbuffers_metadata_dir() { + local out_dir="$1" + for golden in "${golden_files[@]}"; do + validate_xflatbuffers_metadata_file "${out_dir}/${golden}" + done +} + compare_output() { local out_dir="$1" for golden in "${golden_files[@]}"; do @@ -118,6 +389,7 @@ run_case_xflatbuffers() { mkdir -p "${out_dir}" ( cd "${script_dir}" && "${flatc}" "$@" "${include_flags[@]}" -o "${out_dir}" "${schemas[@]}" ) compare_output_stripping_xflatbuffers "${out_dir}" + validate_xflatbuffers_metadata_dir "${out_dir}" } run_case_roundtrip_golden() { @@ -146,6 +418,8 @@ run_case_roundtrip_xflatbuffers() { ( cd "${gen_dir}" && "${flatc}" "$@" -o "${out_dir}" "${golden_files[@]}" ) compare_output_against_dir "${gen_dir}" "${out_dir}" + validate_xflatbuffers_metadata_dir "${gen_dir}" + validate_xflatbuffers_metadata_dir "${out_dir}" } tmp_default="$(mktemp -d)" From 775e9fdeea4d09bb20a56c567d493052ae83379c Mon Sep 17 00:00:00 2001 From: TJKoury Date: Thu, 18 Dec 2025 15:34:14 -0500 Subject: [PATCH 4/5] updated with json schema import --- src/idl_gen_json_schema.cpp | 95 +- src/idl_parser.cpp | 901 ++++++++++++++---- tests/JsonSchemaImportTest.sh | 54 ++ ...ircraft_mission_tasking_subset.schema.json | 281 ++++++ .../udl_openapi_ais_subset.schema.json | 303 ++++++ .../udl_openapi_antenna_subset.schema.json | 619 ++++++++++++ ...ircraft_mission_tasking_subset.schema.json | 391 ++++++++ .../inputs/udl_openapi_ais_subset.schema.json | 375 ++++++++ .../udl_openapi_antenna_subset.schema.json | 778 +++++++++++++++ 9 files changed, 3587 insertions(+), 210 deletions(-) create mode 100755 tests/JsonSchemaImportTest.sh create mode 100644 tests/jsonschema_import/goldens/udl_openapi_aircraft_mission_tasking_subset.schema.json create mode 100644 tests/jsonschema_import/goldens/udl_openapi_ais_subset.schema.json create mode 100644 tests/jsonschema_import/goldens/udl_openapi_antenna_subset.schema.json create mode 100644 tests/jsonschema_import/inputs/udl_openapi_aircraft_mission_tasking_subset.schema.json create mode 100644 tests/jsonschema_import/inputs/udl_openapi_ais_subset.schema.json create mode 100644 tests/jsonschema_import/inputs/udl_openapi_antenna_subset.schema.json diff --git a/src/idl_gen_json_schema.cpp b/src/idl_gen_json_schema.cpp index 706016441e9..70763835e42 100644 --- a/src/idl_gen_json_schema.cpp +++ b/src/idl_gen_json_schema.cpp @@ -166,6 +166,22 @@ class JsonSchemaGenerator : public BaseGenerator { return parser_.opts.jsonschema_include_xflatbuffers; } + const Value *LookupAttribute(const Definition &def, + const char *key) const { + return def.attributes.Lookup(key); + } + + const std::string *LookupAttributeString(const Definition &def, + const char *key) const { + const auto *attr = LookupAttribute(def, key); + return attr ? &attr->constant : nullptr; + } + + bool AttributeIsTrue(const Definition &def, const char *key) const { + const auto *attr = LookupAttribute(def, key); + return attr && (attr->constant == "true" || attr->constant == "1"); + } + std::string JsonString(const std::string& value) const { std::string escaped; if (EscapeString(value.c_str(), value.length(), &escaped, @@ -491,6 +507,20 @@ class JsonSchemaGenerator : public BaseGenerator { NumToString(property->value.type.fixed_length) + "," + NewLine() + Indent(8) + "\"maxItems\": " + NumToString(property->value.type.fixed_length); + } else if (IsVector(property->value.type)) { + if (const auto* min_items = + LookupAttributeString(*property, "jsonschema_minItems")) { + arrayInfo += "," + NewLine() + Indent(8) + "\"minItems\": " + + *min_items; + } + if (const auto* max_items = + LookupAttributeString(*property, "jsonschema_maxItems")) { + arrayInfo += "," + NewLine() + Indent(8) + "\"maxItems\": " + + *max_items; + } + } + if (AttributeIsTrue(*property, "jsonschema_uniqueItems")) { + arrayInfo += "," + NewLine() + Indent(8) + "\"uniqueItems\": true"; } std::string deprecated_info = ""; if (property->deprecated) { @@ -505,7 +535,62 @@ class JsonSchemaGenerator : public BaseGenerator { } std::string typeLine = Indent(4) + "\"" + property->name + "\""; typeLine += " : {" + NewLine() + Indent(8); - typeLine += GenType(property->value.type); + const auto* format = + LookupAttributeString(*property, "jsonschema_format"); + const auto* min_length = + LookupAttributeString(*property, "jsonschema_minLength"); + const auto* max_length = + LookupAttributeString(*property, "jsonschema_maxLength"); + const auto* minimum = + LookupAttributeString(*property, "jsonschema_minimum"); + const auto* maximum = + LookupAttributeString(*property, "jsonschema_maximum"); + const auto* exclusive_min = + LookupAttributeString(*property, "jsonschema_exclusiveMinimum"); + const auto* exclusive_max = + LookupAttributeString(*property, "jsonschema_exclusiveMaximum"); + + const bool is_scalar_integer = + property->value.type.enum_def == nullptr && + IsInteger(property->value.type.base_type); + const bool suppress_integer_range = + is_scalar_integer && + (format || minimum || maximum || exclusive_min || exclusive_max); + if (suppress_integer_range) { + typeLine += GenType("integer"); + } else { + typeLine += GenType(property->value.type); + } + + if (format) { + typeLine += "," + NewLine() + Indent(8) + "\"format\" : " + + JsonString(*format); + } + if (min_length) { + typeLine += "," + NewLine() + Indent(8) + "\"minLength\" : " + + *min_length; + } + if (max_length) { + typeLine += "," + NewLine() + Indent(8) + "\"maxLength\" : " + + *max_length; + } + if (exclusive_min) { + typeLine += "," + NewLine() + Indent(8) + + "\"exclusiveMinimum\" : " + *exclusive_min; + } else if (minimum) { + typeLine += "," + NewLine() + Indent(8) + "\"minimum\" : " + + *minimum; + } + if (exclusive_max) { + typeLine += "," + NewLine() + Indent(8) + + "\"exclusiveMaximum\" : " + *exclusive_max; + } else if (maximum) { + typeLine += "," + NewLine() + Indent(8) + "\"maximum\" : " + + *maximum; + } + if (AttributeIsTrue(*property, "jsonschema_readOnly")) { + typeLine += "," + NewLine() + Indent(8) + "\"readOnly\" : true"; + } typeLine += arrayInfo; typeLine += deprecated_info; typeLine += flatbuffers_info; @@ -539,7 +624,13 @@ class JsonSchemaGenerator : public BaseGenerator { required_string.append("],"); code_ += required_string + NewLine(); } - code_ += Indent(3) + "\"additionalProperties\" : false" + NewLine(); + const auto* additional_props = + LookupAttributeString(*structure, "jsonschema_additionalProperties"); + const bool allow_additional = + additional_props && (*additional_props == "true" || *additional_props == "1"); + if (!allow_additional) { + code_ += Indent(3) + "\"additionalProperties\" : false" + NewLine(); + } auto closeType(Indent(2) + "}"); if (*s != parser_.structs_.vec.back()) { closeType.append(","); diff --git a/src/idl_parser.cpp b/src/idl_parser.cpp index 001ea70b49d..0c4c98b7344 100644 --- a/src/idl_parser.cpp +++ b/src/idl_parser.cpp @@ -3995,6 +3995,36 @@ CheckedError Parser::DoParseJsonSchema() { Type type; bool deprecated = false; std::string description; + bool has_enum = false; + std::vector enum_values; + bool has_format = false; + std::string format; + bool has_min_length = false; + int64_t min_length = 0; + bool has_max_length = false; + int64_t max_length = 0; + bool has_minimum = false; + std::string minimum; + bool has_maximum = false; + std::string maximum; + // OpenAPI 3.0 / JSON Schema draft-04 style exclusivity flags. + bool has_exclusive_minimum = false; + bool exclusive_minimum = false; + bool has_exclusive_maximum = false; + bool exclusive_maximum = false; + // JSON Schema 2019-09+ style exclusive bounds (numeric). + bool has_exclusive_minimum_number = false; + std::string exclusive_minimum_number; + bool has_exclusive_maximum_number = false; + std::string exclusive_maximum_number; + bool has_read_only = false; + bool read_only = false; + bool has_unique_items = false; + bool unique_items = false; + bool has_min_items = false; + int64_t min_items = 0; + bool has_max_items = false; + int64_t max_items = 0; FieldXfbMeta xfb; bool is_anyof = false; std::vector anyof_types; @@ -4030,11 +4060,13 @@ CheckedError Parser::DoParseJsonSchema() { return NoError(); }; - auto ParseUInt64 = [&](uint64_t* out) -> CheckedError { - if (token_ != kTokenIntegerConstant) return Error("integer constant expected"); - if (!StringToNumber(attribute_.c_str(), out)) { - return Error("invalid integer constant: " + attribute_); + auto ParseNumberString = [&](std::string* out, + bool* out_is_integer) -> CheckedError { + if (token_ != kTokenIntegerConstant && token_ != kTokenFloatConstant) { + return Error("number constant expected"); } + *out = attribute_; + if (out_is_integer) *out_is_integer = (token_ == kTokenIntegerConstant); NEXT(); return NoError(); }; @@ -4082,11 +4114,18 @@ CheckedError Parser::DoParseJsonSchema() { auto ExtractDefinitionRef = [&](const std::string& ref, std::string* out) -> CheckedError { - static const std::string kPrefix = "#/definitions/"; - if (ref.rfind(kPrefix, 0) != 0) return Error("unsupported $ref: " + ref); - *out = ref.substr(kPrefix.size()); - if (out->empty()) return Error("unsupported $ref: " + ref); - return NoError(); + static const std::string kDefinitionsPrefix = "#/definitions/"; + static const std::string kDefsPrefix = "#/$defs/"; + static const std::string kOpenApiSchemasPrefix = "#/components/schemas/"; + const std::string* prefixes[] = {&kDefinitionsPrefix, &kDefsPrefix, + &kOpenApiSchemasPrefix}; + for (auto prefix : prefixes) { + if (ref.rfind(*prefix, 0) != 0) continue; + *out = ref.substr(prefix->size()); + if (out->empty()) return Error("unsupported $ref: " + ref); + return NoError(); + } + return Error("unsupported $ref: " + ref); }; auto BaseTypeFromScalarName = [&](const std::string& name, @@ -4136,51 +4175,72 @@ CheckedError Parser::DoParseJsonSchema() { auto InferIntegerBaseType = [&](int64_t min_value, uint64_t max_value, BaseType* out) -> bool { - if (min_value == flatbuffers::numeric_limits::min() && - max_value == static_cast( - flatbuffers::numeric_limits::max())) { + if (min_value >= 0 && + static_cast(min_value) > max_value) { + return false; + } + + if (min_value < 0) { + if (max_value > + static_cast(flatbuffers::numeric_limits::max())) { + return false; + } + const int64_t max_i64 = static_cast(max_value); + if (min_value >= flatbuffers::numeric_limits::min() && + max_i64 <= flatbuffers::numeric_limits::max()) { + *out = BASE_TYPE_CHAR; + return true; + } + if (min_value >= flatbuffers::numeric_limits::min() && + max_i64 <= flatbuffers::numeric_limits::max()) { + *out = BASE_TYPE_SHORT; + return true; + } + if (min_value >= flatbuffers::numeric_limits::min() && + max_i64 <= flatbuffers::numeric_limits::max()) { + *out = BASE_TYPE_INT; + return true; + } + *out = BASE_TYPE_LONG; + return true; + } + + // Non-negative integers: prefer the smallest width; use unsigned only if + // signed of that width can't represent `max_value`. + if (max_value <= + static_cast(flatbuffers::numeric_limits::max())) { *out = BASE_TYPE_CHAR; return true; } - if (min_value == 0 && - max_value == flatbuffers::numeric_limits::max()) { + if (max_value <= flatbuffers::numeric_limits::max()) { *out = BASE_TYPE_UCHAR; return true; } - if (min_value == flatbuffers::numeric_limits::min() && - max_value == static_cast( - flatbuffers::numeric_limits::max())) { + if (max_value <= + static_cast(flatbuffers::numeric_limits::max())) { *out = BASE_TYPE_SHORT; return true; } - if (min_value == 0 && - max_value == flatbuffers::numeric_limits::max()) { + if (max_value <= flatbuffers::numeric_limits::max()) { *out = BASE_TYPE_USHORT; return true; } - if (min_value == flatbuffers::numeric_limits::min() && - max_value == static_cast( - flatbuffers::numeric_limits::max())) { + if (max_value <= + static_cast(flatbuffers::numeric_limits::max())) { *out = BASE_TYPE_INT; return true; } - if (min_value == 0 && - max_value == flatbuffers::numeric_limits::max()) { + if (max_value <= flatbuffers::numeric_limits::max()) { *out = BASE_TYPE_UINT; return true; } - if (min_value == flatbuffers::numeric_limits::min() && - max_value == static_cast( - flatbuffers::numeric_limits::max())) { + if (max_value <= + static_cast(flatbuffers::numeric_limits::max())) { *out = BASE_TYPE_LONG; return true; } - if (min_value == 0 && - max_value == flatbuffers::numeric_limits::max()) { - *out = BASE_TYPE_ULONG; - return true; - } - return false; + *out = BASE_TYPE_ULONG; + return true; }; std::map enum_by_fullname; @@ -4513,21 +4573,46 @@ CheckedError Parser::DoParseJsonSchema() { out->xfb.union_value_field.clear(); out->deprecated = false; out->description.clear(); + out->has_enum = false; + out->enum_values.clear(); + out->has_format = false; + out->format.clear(); + out->has_min_length = false; + out->min_length = 0; + out->has_max_length = false; + out->max_length = 0; + out->has_minimum = false; + out->minimum.clear(); + out->has_maximum = false; + out->maximum.clear(); + out->has_exclusive_minimum = false; + out->exclusive_minimum = false; + out->has_exclusive_maximum = false; + out->exclusive_maximum = false; + out->has_exclusive_minimum_number = false; + out->exclusive_minimum_number.clear(); + out->has_exclusive_maximum_number = false; + out->exclusive_maximum_number.clear(); + out->has_read_only = false; + out->read_only = false; + out->has_unique_items = false; + out->unique_items = false; + out->has_min_items = false; + out->min_items = 0; + out->has_max_items = false; + out->max_items = 0; out->is_anyof = false; out->anyof_types.clear(); std::string ref_fullname; std::string type_name; - bool has_min = false; - bool has_max = false; + bool has_min_int = false; + bool has_max_u64 = false; + bool max_is_negative = false; int64_t min_value = 0; uint64_t max_value = 0; bool has_items = false; Type items_type; - bool has_min_items = false; - bool has_max_items = false; - int64_t min_items = 0; - int64_t max_items = 0; size_t fieldn = 0; auto err = ParseTableDelimiters( @@ -4541,12 +4626,61 @@ CheckedError Parser::DoParseJsonSchema() { ECHECK(ExtractDefinitionRef(ref, &ref_fullname)); } else if (key == "type") { ECHECK(ParseString(&type_name)); + } else if (key == "format") { + out->has_format = true; + ECHECK(ParseString(&out->format)); + } else if (key == "enum") { + out->has_enum = true; + ECHECK(ParseStringArray(&out->enum_values)); + } else if (key == "minLength") { + out->has_min_length = true; + ECHECK(ParseInt64(&out->min_length)); + } else if (key == "maxLength") { + out->has_max_length = true; + ECHECK(ParseInt64(&out->max_length)); + } else if (key == "readOnly") { + out->has_read_only = true; + ECHECK(ParseBool(&out->read_only)); } else if (key == "minimum") { - has_min = true; - ECHECK(ParseInt64(&min_value)); + out->has_minimum = true; + bool is_integer = false; + ECHECK(ParseNumberString(&out->minimum, &is_integer)); + if (is_integer) { + has_min_int = true; + if (!StringToNumber(out->minimum.c_str(), &min_value)) { + return Error("invalid minimum: " + out->minimum); + } + } } else if (key == "maximum") { - has_max = true; - ECHECK(ParseUInt64(&max_value)); + out->has_maximum = true; + bool is_integer = false; + ECHECK(ParseNumberString(&out->maximum, &is_integer)); + if (is_integer) { + if (!out->maximum.empty() && out->maximum[0] == '-') { + max_is_negative = true; + } else { + has_max_u64 = true; + if (!StringToNumber(out->maximum.c_str(), &max_value)) { + return Error("invalid maximum: " + out->maximum); + } + } + } + } else if (key == "exclusiveMinimum") { + if (token_ == kTokenIntegerConstant || token_ == kTokenFloatConstant) { + out->has_exclusive_minimum_number = true; + ECHECK(ParseNumberString(&out->exclusive_minimum_number, nullptr)); + } else { + out->has_exclusive_minimum = true; + ECHECK(ParseBool(&out->exclusive_minimum)); + } + } else if (key == "exclusiveMaximum") { + if (token_ == kTokenIntegerConstant || token_ == kTokenFloatConstant) { + out->has_exclusive_maximum_number = true; + ECHECK(ParseNumberString(&out->exclusive_maximum_number, nullptr)); + } else { + out->has_exclusive_maximum = true; + ECHECK(ParseBool(&out->exclusive_maximum)); + } } else if (key == "items") { has_items = true; ParsedField items; @@ -4554,11 +4688,14 @@ CheckedError Parser::DoParseJsonSchema() { ECHECK(ParseFieldSchemaRef(&items, ParseFieldSchemaRef)); items_type = items.type; } else if (key == "minItems") { - has_min_items = true; - ECHECK(ParseInt64(&min_items)); + out->has_min_items = true; + ECHECK(ParseInt64(&out->min_items)); } else if (key == "maxItems") { - has_max_items = true; - ECHECK(ParseInt64(&max_items)); + out->has_max_items = true; + ECHECK(ParseInt64(&out->max_items)); + } else if (key == "uniqueItems") { + out->has_unique_items = true; + ECHECK(ParseBool(&out->unique_items)); } else if (key == "anyOf") { out->is_anyof = true; ECHECK(ParseAnyOfRefs(&out->anyof_types)); @@ -4590,34 +4727,55 @@ CheckedError Parser::DoParseJsonSchema() { if (type_name == "integer") { BaseType bt = BASE_TYPE_INT; - if (has_min && has_max) InferIntegerBaseType(min_value, max_value, &bt); + if (out->has_format) { + if (out->format == "int32") { + bt = BASE_TYPE_INT; + } else if (out->format == "int64") { + bt = BASE_TYPE_LONG; + } else if (out->format == "uint32") { + bt = BASE_TYPE_UINT; + } else if (out->format == "uint64") { + bt = BASE_TYPE_ULONG; + } + } else if (has_min_int && has_max_u64 && !max_is_negative) { + InferIntegerBaseType(min_value, max_value, &bt); + } out->type = Type(bt); return NoError(); } if (type_name == "number") { - out->type = Type(BASE_TYPE_FLOAT); + BaseType bt = BASE_TYPE_FLOAT; + if (out->has_format) { + if (out->format == "double") { + bt = BASE_TYPE_DOUBLE; + } else if (out->format == "float") { + bt = BASE_TYPE_FLOAT; + } + } + out->type = Type(bt); return NoError(); } if (type_name == "boolean") { out->type = Type(BASE_TYPE_BOOL); return NoError(); } - if (type_name == "string") { + if (type_name == "string" || (type_name.empty() && out->has_enum)) { out->type = Type(BASE_TYPE_STRING); return NoError(); } if (type_name == "array") { if (!has_items) return Error("array missing 'items'"); - const bool is_fixed = has_min_items && has_max_items && - min_items == max_items && min_items >= 0; + const bool is_fixed = out->has_min_items && out->has_max_items && + out->min_items == out->max_items && + out->min_items >= 0; if (is_fixed) { - if (min_items < 1 || - min_items > flatbuffers::numeric_limits::max()) { + if (out->min_items < 1 || + out->min_items > flatbuffers::numeric_limits::max()) { return Error("fixed array length out of range"); } out->type = Type(BASE_TYPE_ARRAY, items_type.struct_def, items_type.enum_def, - static_cast(min_items)); + static_cast(out->min_items)); } else { out->type = Type(BASE_TYPE_VECTOR, items_type.struct_def, items_type.enum_def); @@ -4668,87 +4826,124 @@ CheckedError Parser::DoParseJsonSchema() { : schema_filename.c_str())); bool found_definitions = false; - size_t fieldn = 0; - auto root_err = ParseTableDelimiters( - fieldn, nullptr, - [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { - if (key != "definitions") { - ECHECK(SkipAnyJsonValue()); - return NoError(); - } - found_definitions = true; - size_t defn = 0; - auto defs_err = ParseTableDelimiters( - defn, nullptr, - [&](const std::string& def_fullname, size_t&, - const StructDef*) -> CheckedError { - std::string def_type; - DefinitionXfbMeta xfb; - size_t dn = 0; - auto def_err = ParseTableDelimiters( - dn, nullptr, - [&](const std::string& dk, size_t&, - const StructDef*) -> CheckedError { - if (dk == "type") { - ECHECK(ParseString(&def_type)); - } else if (dk == "x-flatbuffers") { - ECHECK(ParseDefinitionXfbMeta(&xfb)); - } else { - ECHECK(SkipAnyJsonValue()); - } - return NoError(); - }); - ECHECK(def_err); - - if (def_type.empty()) { - return Error("definition missing 'type': " + def_fullname); - } + auto ParseDefinitionsTable = [&]() -> CheckedError { + found_definitions = true; + size_t defn = 0; + auto defs_err = ParseTableDelimiters( + defn, nullptr, + [&](const std::string& def_fullname, size_t&, + const StructDef*) -> CheckedError { + std::string def_type; + bool has_anyof = false; + DefinitionXfbMeta xfb; + size_t dn = 0; + auto def_err = ParseTableDelimiters( + dn, nullptr, + [&](const std::string& dk, size_t&, + const StructDef*) -> CheckedError { + if (dk == "type") { + ECHECK(ParseString(&def_type)); + } else if (dk == "anyOf") { + has_anyof = true; + ECHECK(SkipAnyJsonValue()); + } else if (dk == "x-flatbuffers") { + ECHECK(ParseDefinitionXfbMeta(&xfb)); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(def_err); + + if (def_type.empty()) { + if (has_anyof) { + // OpenAPI/JSON Schema unions are commonly expressed as `anyOf` + // without an explicit `type`. Treat as a FlatBuffers union. + def_type = "string"; + } else { + return Error("definition missing 'type': " + def_fullname); + } + } - std::string decl_name = - xfb.present && !xfb.name.empty() ? xfb.name : def_fullname; - std::vector decl_namespace = - xfb.present ? xfb.name_space : std::vector(); + std::string decl_name = + xfb.present && !xfb.name.empty() ? xfb.name : def_fullname; + std::vector decl_namespace = + xfb.present ? xfb.name_space : std::vector(); + + if (xfb.present && + (!xfb.name.empty() || !xfb.name_space.empty())) { + const auto expected = + MakeSchemaFullName(decl_namespace, decl_name); + if (expected != def_fullname) { + return Error("definition name mismatch: '" + def_fullname + + "' vs '" + expected + "'"); + } + } - if (xfb.present && (!xfb.name.empty() || !xfb.name_space.empty())) { - const auto expected = MakeSchemaFullName(decl_namespace, decl_name); - if (expected != def_fullname) { - return Error("definition name mismatch: '" + def_fullname + - "' vs '" + expected + "'"); - } + return WithNamespace(decl_namespace, [&]() -> CheckedError { + if (def_type == "string") { + const bool is_union = + has_anyof || (xfb.present && xfb.kind == "union"); + EnumDef* enum_def = nullptr; + ECHECK(StartEnum(decl_name, is_union, &enum_def)); + enum_by_fullname[def_fullname] = enum_def; + } else if (def_type == "object") { + StructDef* struct_def = nullptr; + ECHECK(StartStruct(decl_name, &struct_def)); + if (xfb.present) { + if (xfb.kind == "struct") struct_def->fixed = true; + if (xfb.kind == "table") struct_def->fixed = false; + if (xfb.has_key) struct_def->has_key = true; + } else { + struct_def->fixed = false; } + struct_def->sortbysize = !struct_def->fixed; + struct_by_fullname[def_fullname] = struct_def; + } else { + return Error("unsupported definition type: " + def_type); + } + return NoError(); + }); + }); + ECHECK(defs_err); + return NoError(); + }; - return WithNamespace(decl_namespace, [&]() -> CheckedError { - if (def_type == "string") { - const bool is_union = xfb.present && xfb.kind == "union"; - EnumDef* enum_def = nullptr; - ECHECK(StartEnum(decl_name, is_union, &enum_def)); - enum_by_fullname[def_fullname] = enum_def; - } else if (def_type == "object") { - StructDef* struct_def = nullptr; - ECHECK(StartStruct(decl_name, &struct_def)); - if (xfb.present) { - if (xfb.kind == "struct") struct_def->fixed = true; - if (xfb.kind == "table") struct_def->fixed = false; - if (xfb.has_key) struct_def->has_key = true; - } else { - struct_def->fixed = false; - } - struct_def->sortbysize = !struct_def->fixed; - struct_by_fullname[def_fullname] = struct_def; - } else { - return Error("unsupported definition type: " + def_type); + size_t fieldn = 0; + auto root_err = ParseTableDelimiters( + fieldn, nullptr, + [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { + if (key == "definitions" || key == "$defs") { + return ParseDefinitionsTable(); + } + if (key == "components") { + size_t cn = 0; + auto comp_err = ParseTableDelimiters( + cn, nullptr, + [&](const std::string& ck, size_t&, + const StructDef*) -> CheckedError { + if (ck == "schemas") { + return ParseDefinitionsTable(); } + ECHECK(SkipAnyJsonValue()); return NoError(); }); - }); - ECHECK(defs_err); + ECHECK(comp_err); + return NoError(); + } + + ECHECK(SkipAnyJsonValue()); return NoError(); }); ECHECK(root_err); if (opts.require_json_eof) EXPECT(kTokenEof); - if (!found_definitions) return Error("JSON Schema missing 'definitions'"); + if (!found_definitions) { + return Error( + "JSON Schema missing 'definitions' (or '$defs' / " + "OpenAPI 'components.schemas')"); + } return NoError(); }; @@ -4836,10 +5031,61 @@ CheckedError Parser::DoParseJsonSchema() { return NoError(); }; + auto FillUnionEnumFromAnyOf = + [&](EnumDef& enum_def, const DefinitionXfbMeta& xfb, + const std::vector& anyof_types) -> CheckedError { + if (enum_def.size()) return Error("enum redefinition: " + enum_def.name); + + ECHECK(ApplyEnumMeta(enum_def, xfb)); + enum_def.is_union = true; + enum_def.underlying_type.base_type = BASE_TYPE_UTYPE; + enum_def.underlying_type.enum_def = &enum_def; + + EnumValBuilder evb(*this, enum_def); + evb.CreateEnumerator("NONE"); + ECHECK(evb.AcceptEnumerator("NONE")); + + for (const auto& type_fullname : anyof_types) { + auto sit = struct_by_fullname.find(type_fullname); + if (sit == struct_by_fullname.end()) { + return Error("unknown union anyOf type: " + type_fullname); + } + auto* ev = evb.CreateEnumerator(sit->second->name); + ev->union_type = Type(BASE_TYPE_STRUCT, sit->second); + ECHECK(evb.AcceptEnumerator(sit->second->name)); + } + + enum_def.SortByValue(); + return NoError(); + }; + + auto AddAttribute = [&](SymbolTable& attrs, const std::string& key, + const std::string& constant) { + known_attributes_[key] = false; + if (auto* existing = attrs.Lookup(key)) { + existing->constant = constant; + return; + } + auto* v = new Value(); + v->constant = constant; + attrs.Add(key, v); + }; + + auto AddBoolAttribute = [&](SymbolTable& attrs, const std::string& key, + bool value) { + AddAttribute(attrs, key, value ? "true" : "false"); + }; + + auto AddInt64Attribute = [&](SymbolTable& attrs, const std::string& key, + int64_t value) { + AddAttribute(attrs, key, NumToString(value)); + }; + auto FillStruct = [&](StructDef& struct_def, const DefinitionXfbMeta& xfb, const std::vector& fields, const std::vector& required_fields, + bool additional_properties, const std::string& description) -> CheckedError { if (!struct_def.fields.vec.empty()) { return Error("struct redefinition: " + struct_def.name); @@ -4847,17 +5093,42 @@ CheckedError Parser::DoParseJsonSchema() { if (!description.empty()) struct_def.doc_comment = SplitLines(description); + // Preserve JSON Schema object-level settings. + AddBoolAttribute(struct_def.attributes, "jsonschema_additionalProperties", + additional_properties); + if (xfb.present && !xfb.kind.empty()) { if (xfb.kind == "struct") struct_def.fixed = true; if (xfb.kind == "table") struct_def.fixed = false; if (xfb.has_key) struct_def.has_key = true; } else { - for (const auto& field : fields) { - if (field.type.base_type == BASE_TYPE_ARRAY) { - struct_def.fixed = true; - break; + auto IsStructSafeType = [&](const Type& type) { + if (IsScalar(type.base_type)) return true; + if (type.base_type == BASE_TYPE_STRUCT && type.struct_def) { + return type.struct_def->fixed; + } + if (type.base_type == BASE_TYPE_ARRAY) { + const auto elem = type.VectorType(); + if (IsScalar(elem.base_type)) return true; + if (elem.base_type == BASE_TYPE_STRUCT && elem.struct_def) { + return elem.struct_def->fixed; + } + return false; } + return false; + }; + + bool has_fixed_array = false; + bool struct_safe = true; + for (const auto& field : fields) { + if (field.type.base_type == BASE_TYPE_ARRAY) has_fixed_array = true; + if (!IsStructSafeType(field.type)) struct_safe = false; } + + // FlatBuffers fixed-length arrays are only legal inside structs. For JSON + // Schema inputs, only infer "struct" when the definition is otherwise + // struct-safe. + struct_def.fixed = has_fixed_array && struct_safe; } struct_def.sortbysize = !struct_def.fixed; @@ -4866,8 +5137,46 @@ CheckedError Parser::DoParseJsonSchema() { std::vector>> pending_unions; for (const auto& parsed : fields) { + Type field_type = parsed.type; + + // Inline string enums become generated FlatBuffers enums. + if (parsed.has_enum && field_type.base_type == BASE_TYPE_STRING && + !parsed.enum_values.empty()) { + auto* prev_namespace = current_namespace_; + current_namespace_ = struct_def.defined_namespace; + + const std::string base_name = struct_def.name + "_" + parsed.name; + std::string enum_name = base_name; + for (int n = 0; + enums_.Lookup(current_namespace_->GetFullyQualifiedName(enum_name)); + ++n) { + enum_name = base_name + "_" + NumToString(n); + } + + EnumDef* enum_def = nullptr; + ECHECK(StartEnum(enum_name, /*is_union=*/false, &enum_def)); + current_namespace_ = prev_namespace; + + DefinitionXfbMeta empty_meta; + ECHECK(FillEnum(*enum_def, empty_meta, parsed.enum_values)); + field_type = enum_def->underlying_type; + } + + // If we inferred a fixed-length array but the container is a table, + // represent it as a vector and preserve length constraints via + // jsonschema_minItems/jsonschema_maxItems. + const auto fixed_length_constraints = field_type.fixed_length; + const bool demote_fixed_array_to_vector = + !struct_def.fixed && field_type.base_type == BASE_TYPE_ARRAY; + if (demote_fixed_array_to_vector) { + Type vector_type(BASE_TYPE_VECTOR, field_type.struct_def, + field_type.enum_def); + vector_type.element = field_type.element; + field_type = vector_type; + } + FieldDef* field_def = nullptr; - ECHECK(AddField(struct_def, parsed.name, parsed.type, &field_def)); + ECHECK(AddField(struct_def, parsed.name, field_type, &field_def)); field_by_name[parsed.name] = field_def; if (parsed.deprecated) field_def->deprecated = true; @@ -4875,6 +5184,133 @@ CheckedError Parser::DoParseJsonSchema() { field_def->doc_comment = SplitLines(parsed.description); } + if (parsed.has_format && !parsed.format.empty()) { + AddAttribute(field_def->attributes, "jsonschema_format", parsed.format); + } + if (parsed.has_min_length) { + AddInt64Attribute(field_def->attributes, "jsonschema_minLength", + parsed.min_length); + } + if (parsed.has_max_length) { + AddInt64Attribute(field_def->attributes, "jsonschema_maxLength", + parsed.max_length); + } + if (parsed.has_read_only && parsed.read_only) { + AddBoolAttribute(field_def->attributes, "jsonschema_readOnly", true); + } + if (parsed.has_unique_items && parsed.unique_items) { + AddBoolAttribute(field_def->attributes, "jsonschema_uniqueItems", true); + } + if (parsed.has_min_items) { + AddInt64Attribute(field_def->attributes, "jsonschema_minItems", + parsed.min_items); + } + if (parsed.has_max_items) { + AddInt64Attribute(field_def->attributes, "jsonschema_maxItems", + parsed.max_items); + } + + // Preserve numeric bounds. Prefer exclusive bound representation when + // available and applicable. + const bool is_integer_scalar = + field_def->value.type.enum_def == nullptr && + IsInteger(field_def->value.type.base_type); + bool skip_default_integer_bounds = false; + if (is_integer_scalar && parsed.has_minimum && parsed.has_maximum && + !parsed.has_exclusive_minimum_number && + !parsed.has_exclusive_maximum_number && + !(parsed.has_exclusive_minimum && parsed.exclusive_minimum) && + !(parsed.has_exclusive_maximum && parsed.exclusive_maximum)) { + std::string expected_min; + std::string expected_max; + switch (field_def->value.type.base_type) { + case BASE_TYPE_CHAR: + expected_min = + NumToString(flatbuffers::numeric_limits::min()); + expected_max = + NumToString(flatbuffers::numeric_limits::max()); + break; + case BASE_TYPE_UCHAR: + expected_min = "0"; + expected_max = + NumToString(flatbuffers::numeric_limits::max()); + break; + case BASE_TYPE_SHORT: + expected_min = + NumToString(flatbuffers::numeric_limits::min()); + expected_max = + NumToString(flatbuffers::numeric_limits::max()); + break; + case BASE_TYPE_USHORT: + expected_min = "0"; + expected_max = + NumToString(flatbuffers::numeric_limits::max()); + break; + case BASE_TYPE_INT: + expected_min = + NumToString(flatbuffers::numeric_limits::min()); + expected_max = + NumToString(flatbuffers::numeric_limits::max()); + break; + case BASE_TYPE_UINT: + expected_min = "0"; + expected_max = + NumToString(flatbuffers::numeric_limits::max()); + break; + case BASE_TYPE_LONG: + expected_min = + NumToString(flatbuffers::numeric_limits::min()); + expected_max = + NumToString(flatbuffers::numeric_limits::max()); + break; + case BASE_TYPE_ULONG: + expected_min = "0"; + expected_max = + NumToString(flatbuffers::numeric_limits::max()); + break; + default: break; + } + if (!expected_min.empty() && !expected_max.empty() && + parsed.minimum == expected_min && parsed.maximum == expected_max) { + skip_default_integer_bounds = true; + } + } + + if (parsed.has_exclusive_minimum_number && + !parsed.exclusive_minimum_number.empty()) { + AddAttribute(field_def->attributes, "jsonschema_exclusiveMinimum", + parsed.exclusive_minimum_number); + } else if (parsed.has_exclusive_minimum && parsed.exclusive_minimum && + parsed.has_minimum && !parsed.minimum.empty()) { + AddAttribute(field_def->attributes, "jsonschema_exclusiveMinimum", + parsed.minimum); + } else if (!skip_default_integer_bounds && parsed.has_minimum && + !parsed.minimum.empty()) { + AddAttribute(field_def->attributes, "jsonschema_minimum", + parsed.minimum); + } + + if (parsed.has_exclusive_maximum_number && + !parsed.exclusive_maximum_number.empty()) { + AddAttribute(field_def->attributes, "jsonschema_exclusiveMaximum", + parsed.exclusive_maximum_number); + } else if (parsed.has_exclusive_maximum && parsed.exclusive_maximum && + parsed.has_maximum && !parsed.maximum.empty()) { + AddAttribute(field_def->attributes, "jsonschema_exclusiveMaximum", + parsed.maximum); + } else if (!skip_default_integer_bounds && parsed.has_maximum && + !parsed.maximum.empty()) { + AddAttribute(field_def->attributes, "jsonschema_maximum", + parsed.maximum); + } + + if (demote_fixed_array_to_vector) { + AddInt64Attribute(field_def->attributes, "jsonschema_minItems", + fixed_length_constraints); + AddInt64Attribute(field_def->attributes, "jsonschema_maxItems", + fixed_length_constraints); + } + ECHECK(ApplyFieldXfb(*field_def, parsed.xfb)); if (parsed.xfb.present && parsed.xfb.has_deprecated) { field_def->deprecated = parsed.xfb.deprecated; @@ -4972,6 +5408,93 @@ CheckedError Parser::DoParseJsonSchema() { }; size_t root_fieldn = 0; + auto ParseAndFillDefinitionsTable = [&]() -> CheckedError { + size_t defn = 0; + auto defs_err = ParseTableDelimiters( + defn, nullptr, + [&](const std::string& def_fullname, size_t&, + const StructDef*) -> CheckedError { + auto eit = enum_by_fullname.find(def_fullname); + auto sit = struct_by_fullname.find(def_fullname); + if (eit == enum_by_fullname.end() && + sit == struct_by_fullname.end()) { + return Error("unknown definition: " + def_fullname); + } + + std::string def_type; + std::string description; + DefinitionXfbMeta xfb; + std::vector enum_values; + std::vector anyof_types; + std::vector required_fields; + std::vector fields; + bool additional_properties = true; + + size_t dn = 0; + auto def_err = ParseTableDelimiters( + dn, nullptr, + [&](const std::string& dk, size_t&, const StructDef*) + -> CheckedError { + if (dk == "type") { + ECHECK(ParseString(&def_type)); + } else if (dk == "description") { + ECHECK(ParseString(&description)); + } else if (dk == "x-flatbuffers") { + ECHECK(ParseDefinitionXfbMeta(&xfb)); + } else if (dk == "enum") { + ECHECK(ParseStringArray(&enum_values)); + } else if (dk == "anyOf") { + ECHECK(ParseAnyOfRefs(&anyof_types)); + } else if (dk == "required") { + ECHECK(ParseStringArray(&required_fields)); + } else if (dk == "additionalProperties") { + ECHECK(ParseBool(&additional_properties)); + } else if (dk == "properties") { + size_t pn = 0; + auto props_err = ParseTableDelimiters( + pn, nullptr, + [&](const std::string& field_name, size_t&, + const StructDef*) -> CheckedError { + ParsedField parsed; + parsed.name = field_name; + ECHECK(ParseFieldSchema(&parsed, ParseFieldSchema)); + fields.push_back(std::move(parsed)); + return NoError(); + }); + ECHECK(props_err); + } else { + ECHECK(SkipAnyJsonValue()); + } + return NoError(); + }); + ECHECK(def_err); + + if (eit != enum_by_fullname.end()) { + if (!def_type.empty() && def_type != "string") { + return Error("enum type must be 'string': " + def_fullname); + } + if (!description.empty()) { + eit->second->doc_comment = SplitLines(description); + } + if (!anyof_types.empty()) { + ECHECK(FillUnionEnumFromAnyOf(*eit->second, xfb, anyof_types)); + } else { + ECHECK(FillEnum(*eit->second, xfb, enum_values)); + } + } else { + if (!def_type.empty() && def_type != "object") { + return Error("struct type must be 'object': " + def_fullname); + } + ECHECK(FillStruct(*sit->second, xfb, fields, required_fields, + additional_properties, description)); + } + + return NoError(); + }); + ECHECK(defs_err); + return NoError(); + }; + auto root_err = ParseTableDelimiters( root_fieldn, nullptr, [&](const std::string& key, size_t&, const StructDef*) -> CheckedError { @@ -4985,96 +5508,58 @@ CheckedError Parser::DoParseJsonSchema() { ECHECK(ParseRootXfbMeta()); return NoError(); } - if (key != "definitions") { - ECHECK(SkipAnyJsonValue()); - return NoError(); + if (key == "definitions" || key == "$defs") { + return ParseAndFillDefinitionsTable(); } - - size_t defn = 0; - auto defs_err = ParseTableDelimiters( - defn, nullptr, - [&](const std::string& def_fullname, size_t&, - const StructDef*) -> CheckedError { - auto eit = enum_by_fullname.find(def_fullname); - auto sit = struct_by_fullname.find(def_fullname); - if (eit == enum_by_fullname.end() && - sit == struct_by_fullname.end()) { - return Error("unknown definition: " + def_fullname); - } - - std::string def_type; - std::string description; - DefinitionXfbMeta xfb; - std::vector enum_values; - std::vector required_fields; - std::vector fields; - - size_t dn = 0; - auto def_err = ParseTableDelimiters( - dn, nullptr, - [&](const std::string& dk, size_t&, const StructDef*) - -> CheckedError { - if (dk == "type") { - ECHECK(ParseString(&def_type)); - } else if (dk == "description") { - ECHECK(ParseString(&description)); - } else if (dk == "x-flatbuffers") { - ECHECK(ParseDefinitionXfbMeta(&xfb)); - } else if (dk == "enum") { - ECHECK(ParseStringArray(&enum_values)); - } else if (dk == "required") { - ECHECK(ParseStringArray(&required_fields)); - } else if (dk == "properties") { - size_t pn = 0; - auto props_err = ParseTableDelimiters( - pn, nullptr, - [&](const std::string& field_name, size_t&, - const StructDef*) -> CheckedError { - ParsedField parsed; - parsed.name = field_name; - ECHECK(ParseFieldSchema(&parsed, ParseFieldSchema)); - fields.push_back(std::move(parsed)); - return NoError(); - }); - ECHECK(props_err); - } else { - ECHECK(SkipAnyJsonValue()); - } - return NoError(); - }); - ECHECK(def_err); - - if (eit != enum_by_fullname.end()) { - if (!def_type.empty() && def_type != "string") { - return Error("enum type must be 'string': " + def_fullname); - } - if (!description.empty()) { - eit->second->doc_comment = SplitLines(description); - } - ECHECK(FillEnum(*eit->second, xfb, enum_values)); - } else { - if (!def_type.empty() && def_type != "object") { - return Error("struct type must be 'object': " + def_fullname); + if (key == "components") { + size_t cn = 0; + auto comp_err = ParseTableDelimiters( + cn, nullptr, + [&](const std::string& ck, size_t&, + const StructDef*) -> CheckedError { + if (ck == "schemas") { + return ParseAndFillDefinitionsTable(); } - ECHECK(FillStruct(*sit->second, xfb, fields, required_fields, - description)); - } + ECHECK(SkipAnyJsonValue()); + return NoError(); + }); + ECHECK(comp_err); + return NoError(); + } - return NoError(); - }); - ECHECK(defs_err); + ECHECK(SkipAnyJsonValue()); return NoError(); }); ECHECK(root_err); if (opts.require_json_eof) EXPECT(kTokenEof); - if (root_ref_fullname.empty()) return Error("JSON Schema missing root '$ref'"); - auto it_root = struct_by_fullname.find(root_ref_fullname); - if (it_root == struct_by_fullname.end()) { - return Error("root '$ref' is not a struct definition: " + root_ref_fullname); + auto LookupRoot = [&](const std::string& name) -> StructDef* { + auto it = struct_by_fullname.find(name); + return it == struct_by_fullname.end() ? nullptr : it->second; + }; + + StructDef* root = nullptr; + if (!root_ref_fullname.empty()) { + root = LookupRoot(root_ref_fullname); + if (!root) { + return Error("root '$ref' is not a struct definition: " + + root_ref_fullname); + } + } else if (!opts.root_type.empty()) { + root = LookupRoot(opts.root_type); + if (!root) { + std::string underscored = opts.root_type; + std::replace(underscored.begin(), underscored.end(), '.', '_'); + root = LookupRoot(underscored); + } + if (!root) return Error("unknown root type: " + opts.root_type); + } else if (!struct_by_fullname.empty()) { + root = struct_by_fullname.begin()->second; } - root_struct_def_ = it_root->second; + + if (!root) return Error("JSON Schema contains no root struct definition"); + root_struct_def_ = root; return NoError(); } diff --git a/tests/JsonSchemaImportTest.sh b/tests/JsonSchemaImportTest.sh new file mode 100755 index 00000000000..326b5fd88cd --- /dev/null +++ b/tests/JsonSchemaImportTest.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -eu + +script_dir="$(cd "$(dirname "$0")" && pwd)" +repo_dir="$(cd "${script_dir}/.." && pwd)" +flatc="${repo_dir}/build/flatc" +if [[ ! -x "${flatc}" ]]; then + flatc="${repo_dir}/flatc" +fi + +if [[ ! -x "${flatc}" ]]; then + echo "Skipping JSON Schema import tests: flatc executable not found at ${flatc}." >&2 + exit 0 +fi + +input_dir="${script_dir}/jsonschema_import/inputs" +golden_dir="${script_dir}/jsonschema_import/goldens" + +if [[ ! -d "${input_dir}" || ! -d "${golden_dir}" ]]; then + echo "Missing JSON Schema import fixtures under ${script_dir}/jsonschema_import." >&2 + exit 1 +fi + +tmp_out="$(mktemp -d)" +tmp_roundtrip="$(mktemp -d)" +cleanup() { + rm -rf "${tmp_out}" "${tmp_roundtrip}" +} +trap cleanup EXIT + +run_case() { + local filename="$1" + shift + local input_path="${input_dir}/${filename}" + local golden_path="${golden_dir}/${filename}" + local out_path="${tmp_out}/${filename}" + local roundtrip_path="${tmp_roundtrip}/${filename}" + + echo "Importing + generating JSON Schema: ${filename}" + "${flatc}" "$@" --jsonschema -o "${tmp_out}" "${input_path}" + diff -u "${golden_path}" "${out_path}" + + echo "Round-tripping generated JSON Schema: ${filename}" + "${flatc}" --jsonschema -o "${tmp_roundtrip}" "${out_path}" + diff -u "${out_path}" "${roundtrip_path}" +} + +run_case "udl_openapi_antenna_subset.schema.json" +run_case "udl_openapi_ais_subset.schema.json" +run_case "udl_openapi_aircraft_mission_tasking_subset.schema.json" \ + --root-type AircraftMissionTasking_Abridged + +echo "JSON Schema import tests passed" + diff --git a/tests/jsonschema_import/goldens/udl_openapi_aircraft_mission_tasking_subset.schema.json b/tests/jsonschema_import/goldens/udl_openapi_aircraft_mission_tasking_subset.schema.json new file mode 100644 index 00000000000..dd6fde56a5b --- /dev/null +++ b/tests/jsonschema_import/goldens/udl_openapi_aircraft_mission_tasking_subset.schema.json @@ -0,0 +1,281 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "definitions": { + "AircraftMissionLocationTasking_Abridged" : { + "type" : "object", + "description" : "Collection of aircraft mission location information for this aircraft mission tasking.", + "properties" : { + "airMsnPri" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The code for the priority assigned to this mission." + }, + "alt" : { + "type" : "integer", + "format" : "int32", + "description" : "The altitude for this mission represented as hundreds of feet above MSL." + }, + "areaGeoRad" : { + "type" : "integer", + "format" : "int32", + "description" : "The radius of the circle around the location being reported in feet." + }, + "endTime" : { + "type" : "string", + "format" : "date-time", + "description" : "The end time of this mission in ISO 8601 UTC format with millisecond precision." + }, + "msnLocName" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 24, + "description" : "The name that identifies the location at which this mission is to be performed. This can be the name of a general target area, orbit, cap point, station, etc." + }, + "msnLocPtBarT" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "The alpha-numeric specified location for this mission specified as a bearing angle in degrees relative to true north and a range in nautical miles (NM)." + }, + "msnLocPtLat" : { + "type" : "number", + "format" : "double", + "minimum" : -90, + "maximum" : 90, + "description" : "WGS-84 latitude of the mission location, in degrees. -90 to 90 degrees (negative values south of equator) for this tasked air mission." + }, + "msnLocPtLon" : { + "type" : "number", + "format" : "double", + "minimum" : -180, + "maximum" : 180, + "description" : "WGS-84 longitude of the mission location, in degrees. -180 to 180 degrees (negative values west of Prime Meridian) for this tasked air mission." + }, + "msnLocPtName" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "The location name for this mission." + }, + "startTime" : { + "type" : "string", + "format" : "date-time", + "description" : "The start time of this mission in ISO 8601 UTC format with millisecond precision." + } + }, + "required" : ["startTime"], + }, + "AircraftMissionTasking_Abridged" : { + "type" : "object", + "description" : "Collection that specifies the tasked country, tasked service, unit and mission level tasking for this ATO.", + "properties" : { + "acMsnLocSeg" : { + "type" : "array", "items" : {"$ref" : "#/definitions/AircraftMissionLocationTasking_Abridged"}, + "description" : "A collection of aircraft mission location information for this aircraft mission tasking." + }, + "alertStatus" : { + "type" : "integer", + "format" : "int32", + "description" : "The readiness status expressed in time (minutes) for an aircraft to be airborne after the launch order is received or the time required for a missile unit to assume battle stations." + }, + "amcMsnNum" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 16, + "description" : "The AMC number assigned to identify one aircraft from another." + }, + "countryCode" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 4, + "description" : "The country code responsible for conducting this aircraft mission tasking for the exercise or operation." + }, + "depLocLat" : { + "type" : "number", + "format" : "double", + "minimum" : -90, + "maximum" : 90, + "description" : "WGS-84 latitude of the departure location, in degrees. -90 to 90 degrees (negative values south of equator) for this tasked air mission." + }, + "depLocLon" : { + "type" : "number", + "format" : "double", + "minimum" : -180, + "maximum" : 180, + "description" : "WGS-84 longitude of the departure location, in degrees. -180 to 180 degrees (negative values west of Prime Meridian) for this tasked air mission." + }, + "depLocName" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "The location or name specified for the departure of the tasked air mission." + }, + "depLocUTM" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "The departure location specified in UTM (100 meter) coordinates for the tasked air mission." + }, + "depTime" : { + "type" : "string", + "format" : "date-time", + "description" : "The time of departure for the tasked air mission in ISO8601 UTC format with millisecond precision." + }, + "indACTasking" : { + "type" : "array", "items" : {"$ref" : "#/definitions/IndividualAircraftTasking_Abridged"}, + "description" : "A collection of the individual aircraft assigned to this aircraft mission tasking." + }, + "msnCommander" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The commander responsible for the planning and execution of the forces necessary to achieve desired objectives." + }, + "msnNum" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The mission number assigned to this mission." + }, + "pkgId" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The identifier for the composite set of missions for this operation/exercise." + }, + "priMsnType" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The code for the preferred type or designator for a tasked air mission." + }, + "rcvyLocLat" : { + "type" : "array", "items" : {"type" : "number"}, + "description" : "An array of WGS-84 latitude of the recovery locations, in degrees. -90 to 90 degrees (negative values south of equator) for this tasked air mission." + }, + "rcvyLocLon" : { + "type" : "array", "items" : {"type" : "number"}, + "description" : "An array of WGS-84 longitude of the recovery locations, in degrees. -180 to 180 degrees (negative values west of Prime Meridian) for this tasked air mission." + }, + "rcvyLocName" : { + "type" : "array", "items" : {"type" : "string"}, + "minItems": 0, + "maxItems": 36, + "description" : "An array of locations specified for the recovery of the tasked air mission represented by varying formats." + }, + "rcvyLocUTM" : { + "type" : "array", "items" : {"type" : "string"}, + "minItems": 0, + "maxItems": 36, + "description" : "An array of recovery locations specified in UTM (100 meter) coordinates for the tasked air mission." + }, + "rcvyTime" : { + "type" : "array", "items" : {"type" : "string"}, + "description" : "An array of recovery times for the tasked air mission in ISO8601 UTC format with millisecond precision." + }, + "resMsnInd" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 1, + "description" : "An indicator of whether a mission is or will be a residual mission." + }, + "secMsnType" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The code for the alternative type of a tasked air mission." + }, + "taskedService" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 1, + "description" : "The service tasked with conducting this aircraft mission tasking for the exercise or operation." + }, + "unitDesignator" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "The designator of the unit that is tasked to perform this aircraft mission tasking." + }, + "unitLocName" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "The tasked units location expressed as an ICAO or a place name." + } + }, + "required" : ["countryCode", "taskedService", "unitDesignator"], + }, + "IndividualAircraftTasking_Abridged" : { + "type" : "object", + "description" : "Collection that specifies the naval flight operations for this ATO.", + "properties" : { + "acftType" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "The type and model number for the aircraft. The field may specify a value of an aircraft not yet assigned an aircraft code contained in the aircraft codes list." + }, + "callSign" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 24, + "description" : "The call sign assigned to this mission aircraft." + }, + "iffSifMode1Code" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The mode 1 and code of the Identification Friend or FOE (IFF) or Selective Identification Feature (SIF)." + }, + "iffSifMode2Code" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The mode 2 and code of the Identification Friend or FOE (IFF) or Selective Identification Feature (SIF)." + }, + "iffSifMode3Code" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The mode 3 and code of the Identification Friend or FOE (IFF) or Selective Identification Feature (SIF)." + }, + "juAddress" : { + "type" : "array", "items" : {"type" : "integer", "minimum" : -2147483648, "maximum" : 2147483647}, + "description" : "An optional array of link 16 octal track numbers assigned as the primary JTIDS Unit (JU) address for the mission aircraft." + }, + "link16CallSign" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 8, + "description" : "The Link 16 abbreviated call sign assigned to the ACA. This is normally the first and last letter and the last two numbers of the call sign." + }, + "numAcft" : { + "type" : "integer", + "format" : "int32", + "description" : "The number of aircraft participating in this mission." + }, + "priConfigCode" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 48, + "description" : "The code that indicates the ordinance mix carried on this mission aircraft." + }, + "secConfigCode" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 48, + "description" : "The code for the secondary ordinance mix carried on this mission aircraft." + }, + "tacanChan" : { + "type" : "integer", + "format" : "int32", + "description" : "The TACAN channel assigned to this mission aircraft." + } + }, + "required" : ["acftType"], + } + }, + "$ref" : "#/definitions/AircraftMissionTasking_Abridged" +} diff --git a/tests/jsonschema_import/goldens/udl_openapi_ais_subset.schema.json b/tests/jsonschema_import/goldens/udl_openapi_ais_subset.schema.json new file mode 100644 index 00000000000..da29bad5c8b --- /dev/null +++ b/tests/jsonschema_import/goldens/udl_openapi_ais_subset.schema.json @@ -0,0 +1,303 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "definitions": { + "AIS_Abridged_dataMode" : { + "type" : "string", + "enum": ["REAL", "TEST", "SIMULATED", "EXERCISE"] + }, + "AIS_Abridged" : { + "type" : "object", + "description" : "Self-reported information obtained from Automatic Identification System (AIS) equipment. This contains information such as unique identification, status, position, course, and speed. The AIS is an automatic tracking system that uses transceivers on ships and is used by vessel traffic services. Although technically and operationally distinct, the AIS system is analogous to ADS-B that performs a similar function for aircraft. AIS is intended to assist a vessel's watchstanding officers and allow maritime authorities to track and monitor vessel movements. AIS integrates a standardized VHF transceiver with a positioning system such as Global Positioning System receiver, with other electronic navigation sensors, such as gyrocompass or rate of turn indicator. Vessels fitted with AIS transceivers can be tracked by AIS base stations located along coast lines or, when out of range of terrestrial networks, through a growing number of satellites that are fitted with special AIS receivers which are capable of deconflicting a large number of signatures.", + "properties" : { + "antennaRefDimensions" : { + "type" : "array", "items" : {"type" : "number"}, + "description" : "The reference dimensions of the vessel, reported as [A, B, C, D], in meters. Where the array values represent the distance fore (A), aft (B), to port (C), and to starboard (D) of the navigation antenna. Array with values A = C = 0 and B, D > 0 indicate the length (B) and width (D) of the vessel without antenna position reference." + }, + "avgSpeed" : { + "type" : "number", + "format" : "double", + "description" : "The average speed, in kilometers/hour, calculated for the subject vessel during the latest voyage (port to port)." + }, + "callSign" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 24, + "description" : "A uniquely designated identifier for the vessel's transmitter station." + }, + "cargoType" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 48, + "description" : "The reported cargo type. Intended as, but not constrained to, the USCG NAVCEN AIS cargo definitions. Users should refer to USCG Navigation Center documentation for specific definitions associated with ship and cargo types. USCG NAVCEN documentation may be found at https://www.navcen.uscg.gov." + }, + "classificationMarking" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 128, + "description" : "Classification marking of the data in IC/CAPCO Portion-marked format." + }, + "course" : { + "type" : "number", + "format" : "double", + "description" : "The course-over-ground reported by the vessel, in degrees." + }, + "createdAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was created in the database, auto-populated by the system." + }, + "createdBy" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who created the row in the database, auto-populated by the system." + }, + "currentPortGUID" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 4, + "description" : "The US Geographic Unique Identifier of the current port hosting the vessel." + }, + "currentPortLOCODE" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 5, + "description" : "The UN Location Code of the current port hosting the vessel." + }, + "dataMode" : { + "$ref" : "#/definitions/AIS_Abridged_dataMode", + "minLength" : 1, + "maxLength" : 32, + "description" : "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n" + }, + "destination" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 20, + "description" : "The destination of the vessel according to the AIS transmission." + }, + "destinationETA" : { + "type" : "string", + "format" : "date-time", + "description" : "The Estimated Time of Arrival of the vessel at the destination, in ISO 8601 UTC format." + }, + "distanceToGo" : { + "type" : "number", + "format" : "double", + "description" : "The remaining distance, in kilometers, for the vessel to reach the reported destination." + }, + "distanceTravelled" : { + "type" : "number", + "format" : "double", + "description" : "The distance, in kilometers, that the vessel has travelled since departing the last port." + }, + "draught" : { + "type" : "number", + "format" : "double", + "description" : "The maximum static draught, in meters, of the vessel according to the AIS transmission." + }, + "engagedIn" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 48, + "description" : "The activity that the vessel is engaged in. This entry applies only when the shipType = Other." + }, + "etaCalculated" : { + "type" : "string", + "format" : "date-time", + "description" : "The Estimated Time of Arrival of the vessel at the destination port, according to MarineTraffic calculations, in ISO 8601 UTC format." + }, + "etaUpdated" : { + "type" : "string", + "format" : "date-time", + "description" : "The date and time that the ETA was calculated by MarineTraffic, in ISO 8601 UTC format." + }, + "id" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "Unique identifier of the record, auto-generated by the system." + }, + "idTrack" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "Unique identifier of the Track." + }, + "idVessel" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "Unique identifier of the vessel." + }, + "imon" : { + "type" : "integer", + "format" : "int64", + "description" : "The International Maritime Organization Number of the vessel. IMON is a seven-digit number that uniquely identifies the vessel." + }, + "lastPortGUID" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 4, + "description" : "The US Geographic Unique Identifier of the last port visited by the vessel." + }, + "lastPortLOCODE" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 5, + "description" : "The UN Location Code of the last port visited by the vessel." + }, + "lat" : { + "type" : "number", + "format" : "double", + "minimum" : -90, + "maximum" : 90, + "description" : "WGS-84 latitude of the vessel position, in degrees. -90 to 90 degrees (negative values south of equator)." + }, + "length" : { + "type" : "number", + "format" : "double", + "description" : "The overall length of the vessel, in meters. A value of 511 indicates a vessel length of 511 meters or greater." + }, + "lon" : { + "type" : "number", + "format" : "double", + "minimum" : -180, + "maximum" : 180, + "description" : "WGS-84 longitude of the vessel position, in degrees. -180 to 180 degrees (negative values west of Prime Meridian)." + }, + "maxSpeed" : { + "type" : "number", + "format" : "double", + "description" : "The maximum speed, in kilometers/hour, reported by the subject vessel during the latest voyage (port to port)." + }, + "mmsi" : { + "type" : "integer", + "format" : "int64", + "description" : "The Maritime Mobile Service Identity of the vessel. MMSI is a nine-digit number that identifies the transmitter station of the vessel." + }, + "navStatus" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "description" : "The AIS Navigational Status of the vessel (e.g. Underway Using Engine, Moored, Aground, etc.). Intended as, but not constrained to, the USCG NAVCEN navigation status definitions. Users should refer to USCG Navigation Center documentation for specific definitions associated with navigation status. USCG NAVCEN documentation may be found at https://www.navcen.uscg.gov." + }, + "nextPortGUID" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 4, + "description" : "The US Geographic Unique Identifier of the next destination port of the vessel." + }, + "nextPortLOCODE" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 5, + "description" : "The UN Location Code of the next destination port of the vessel." + }, + "origNetwork" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 32, + "readOnly" : true, + "description" : "The originating source network on which this record was created, auto-populated by the system." + }, + "origin" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "description" : "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin." + }, + "posDeviceType" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 24, + "description" : "The type of electronic position fixing device (e.g. GPS, GLONASS, etc.). Intended as, but not constrained to, the USCG NAVCEN electronic position fixing device definitions. Users should refer to USCG Navigation Center documentation for specific device type information. USCG NAVCEN documentation may be found at https://www.navcen.uscg.gov." + }, + "posHiAccuracy" : { + "type" : "boolean", + "description" : "Flag indicating high reported position accuracy (less than or equal to 10 meters). A value of 0/false indicates low accuracy (greater than 10 meters)." + }, + "posHiLatency" : { + "type" : "boolean", + "description" : "Flag indicating high reported position latency (greater than 5 seconds). A value of 0/false indicates low latency (less than 5 seconds)." + }, + "rateOfTurn" : { + "type" : "number", + "format" : "double", + "description" : "The Rate-of-Turn for the vessel, in degrees/minute. Positive value indicates that the vessel is turning right." + }, + "shipDescription" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 100, + "description" : "Further description or explanation of the vessel or type." + }, + "shipName" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 24, + "description" : "The name of the vessel. Vessel names that exceed the AIS 20 character are shortened (not truncated) to 15 character-spaces, followed by an underscore and the last 4 characters-spaces of the vessel full name." + }, + "shipType" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 48, + "description" : "The reported ship type (e.g. Passenger, Tanker, Cargo, Other, etc.). See the engagedIn and specialCraft entries for additional information on certain types of vessels." + }, + "source" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "Source of the data." + }, + "sourceDL" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "readOnly" : true, + "description" : "The source data library from which this record was received. This could be a remote or tactical UDL or another data library. If null, the record should be assumed to have originated from the primary Enterprise UDL." + }, + "specialCraft" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 48, + "description" : "The type of special craft designation of the vessel. This entry applies only when the shipType = Special Craft." + }, + "specialManeuver" : { + "type" : "boolean", + "description" : "Flag indicating that the vessel is engaged in a special maneuver (e.g. Waterway Navigation)." + }, + "speed" : { + "type" : "number", + "format" : "double", + "description" : "The speed-over-ground reported by the vessel, in kilometers/hour." + }, + "trueHeading" : { + "type" : "number", + "format" : "double", + "description" : "The true heading reported by the vessel, in degrees." + }, + "ts" : { + "type" : "string", + "format" : "date-time", + "description" : "The timestamp that the vessel position was recorded, in ISO 8601 UTC format." + }, + "vesselFlag" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "The flag of the subject vessel according to AIS transmission." + }, + "width" : { + "type" : "number", + "format" : "double", + "description" : "The breadth of the vessel, in meters. A value of 63 indicates a vessel breadth of 63 meters or greater." + } + }, + "required" : ["classificationMarking", "dataMode", "source", "ts"], + } + }, + "$ref" : "#/definitions/AIS_Abridged" +} diff --git a/tests/jsonschema_import/goldens/udl_openapi_antenna_subset.schema.json b/tests/jsonschema_import/goldens/udl_openapi_antenna_subset.schema.json new file mode 100644 index 00000000000..afbb40cb15f --- /dev/null +++ b/tests/jsonschema_import/goldens/udl_openapi_antenna_subset.schema.json @@ -0,0 +1,619 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "definitions": { + "Antenna_dataMode" : { + "type" : "string", + "enum": ["REAL", "TEST", "SIMULATED", "EXERCISE"] + }, + "AntennaDetails_dataMode" : { + "type" : "string", + "enum": ["REAL", "TEST", "SIMULATED", "EXERCISE"] + }, + "AntennaDetails_mode" : { + "type" : "string", + "enum": ["TX", "RX"] + }, + "Organization_dataMode" : { + "type" : "string", + "enum": ["REAL", "TEST", "SIMULATED", "EXERCISE"] + }, + "OrganizationDetails_dataMode" : { + "type" : "string", + "enum": ["REAL", "TEST", "SIMULATED", "EXERCISE"] + }, + "Antenna" : { + "type" : "object", + "description" : "Model representation of information on on-orbit/spacecraft communication antennas. A spacecraft may have multiple antennas and each antenna can have multiple 'details' records compiled by different sources.", + "properties" : { + "antennaDetails" : { + "type" : "array", "items" : {"$ref" : "#/definitions/AntennaDetails"}, + "readOnly" : true, + "uniqueItems": true, + "description" : "Read-only collection of additional AntennaDetails by various sources for this organization, ignored on create/update. These details must be created separately via the /udl/antennadetails operations." + }, + "createdAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was created in the database, auto-populated by the system." + }, + "createdBy" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who created the row in the database, auto-populated by the system." + }, + "dataMode" : { + "$ref" : "#/definitions/Antenna_dataMode", + "minLength" : 1, + "maxLength" : 32, + "description" : "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n" + }, + "id" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "Unique identifier of the record, auto-generated by the system." + }, + "name" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 128, + "description" : "Antenna name." + }, + "origNetwork" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 32, + "readOnly" : true, + "description" : "The originating source network on which this record was created, auto-populated by the system." + }, + "origin" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "description" : "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin." + }, + "source" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "description" : "Source of the data." + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was last updated in the database, auto-populated by the system." + }, + "updatedBy" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who updated the row in the database, auto-populated by the system." + } + }, + "required" : ["dataMode", "name", "source"], + }, + "AntennaDetails" : { + "type" : "object", + "description" : "Detailed information for a spacecraft communication antenna. One antenna may have multiple AntennaDetails records, compiled by various sources.", + "properties" : { + "beamForming" : { + "type" : "boolean", + "description" : "Boolean indicating if this is a beam forming antenna." + }, + "beamwidth" : { + "type" : "number", + "format" : "double", + "description" : "Array of angles between the half-power (-3 dB) points of the main lobe of the antenna, in degrees." + }, + "classificationMarking" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 128, + "description" : "Classification marking of the data in IC/CAPCO Portion-marked format." + }, + "createdAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was created in the database, auto-populated by the system." + }, + "createdBy" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who created the row in the database, auto-populated by the system." + }, + "dataMode" : { + "$ref" : "#/definitions/AntennaDetails_dataMode", + "minLength" : 1, + "maxLength" : 32, + "description" : "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n" + }, + "description" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 512, + "description" : "Antenna description." + }, + "diameter" : { + "type" : "number", + "format" : "double", + "description" : "Antenna diameter in meters." + }, + "endFrequency" : { + "type" : "number", + "format" : "double", + "description" : "Antenna end of frequency range in Mhz." + }, + "gain" : { + "type" : "number", + "format" : "double", + "description" : "Antenna maximum gain in dBi." + }, + "gainTolerance" : { + "type" : "number", + "format" : "double", + "description" : "Antenna gain tolerance in dB." + }, + "id" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "Unique identifier of the record, auto-generated by the system." + }, + "idAntenna" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "Unique identifier of the parent Antenna." + }, + "manufacturerOrg" : { + "$ref" : "#/definitions/Organization" + }, + "manufacturerOrgId" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "ID of the organization that manufactures the antenna." + }, + "mode" : { + "$ref" : "#/definitions/AntennaDetails_mode", + "minLength" : 0, + "maxLength" : 4, + "description" : "Antenna mode (e.g. TX,RX)." + }, + "origNetwork" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 32, + "readOnly" : true, + "description" : "The originating source network on which this record was created, auto-populated by the system." + }, + "origin" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "description" : "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin." + }, + "polarization" : { + "type" : "number", + "format" : "double", + "description" : "Antenna polarization in degrees." + }, + "position" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 128, + "description" : "Antenna position (e.g. Top, Nadir, Side)." + }, + "size" : { + "type" : "array", "items" : {"type" : "number"}, + "description" : "Array with 1-2 values specifying the length and width (for rectangular) and just length for dipole antennas in meters." + }, + "source" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "description" : "Source of the data." + }, + "startFrequency" : { + "type" : "number", + "format" : "double", + "description" : "Antenna start of frequency range in Mhz." + }, + "steerable" : { + "type" : "boolean", + "description" : "Boolean indicating if this antenna is steerable." + }, + "tags" : { + "type" : "array", "items" : {"type" : "string"}, + "description" : "Optional array of provider/source specific tags for this data, where each element is no longer than 32 characters, used for implementing data owner conditional access controls to restrict access to the data. Should be left null by data providers unless conditional access controls are coordinated with the UDL team." + }, + "type" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "description" : "Type of antenna (e.g. Reflector, Double Reflector, Shaped Reflector, Horn, Parabolic, etc.)." + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was last updated in the database, auto-populated by the system." + }, + "updatedBy" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who updated the row in the database, auto-populated by the system." + } + }, + "required" : ["classificationMarking", "dataMode", "idAntenna", "source"], + }, + "Organization" : { + "type" : "object", + "description" : "An organization such as a corporation, manufacturer, consortium, government, etc. An organization may have parent and child organizations as well as link to a former organization if this org previously existed as another organization.", + "properties" : { + "active" : { + "type" : "boolean", + "description" : "Boolean indicating if this organization is currently active." + }, + "category" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 128, + "description" : "Subtype or category of the organization (e.g. Private company, stock market quoted company, subsidiary, goverment department/agency, etc)." + }, + "classificationMarking" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 128, + "description" : "Classification marking of the data in IC/CAPCO Portion-marked format." + }, + "countryCode" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 4, + "description" : "Country of the physical location of the organization. This value is typically the ISO 3166 Alpha-2 two-character country code. However, it can also represent various consortiums that do not appear in the ISO document. The code must correspond to an existing country in the UDL’s country API. Call udl/country/{code} to get any associated FIPS code, ISO Alpha-3 code, or alternate code values that exist for the specified country code." + }, + "createdAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was created in the database, auto-populated by the system." + }, + "createdBy" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who created the row in the database, auto-populated by the system." + }, + "dataMode" : { + "$ref" : "#/definitions/Organization_dataMode", + "minLength" : 1, + "maxLength" : 32, + "description" : "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n" + }, + "description" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 256, + "description" : "Organization description." + }, + "externalId" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "Optional externally provided identifier for this row." + }, + "id" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "Unique identifier of the record, auto-generated by the system." + }, + "name" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 128, + "description" : "Organization name." + }, + "nationality" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 4, + "description" : "Country of registration or ownership of the organization. This value is typically the ISO 3166 Alpha-2 two-character country code, however it can also represent various consortiums that do not appear in the ISO document. The code must correspond to an existing country in the UDL’s country API. Call udl/country/{code} to get any associated FIPS code, ISO Alpha-3 code, or alternate code values that exist for the specified country code." + }, + "organizationDetails" : { + "type" : "array", "items" : {"$ref" : "#/definitions/OrganizationDetails"}, + "readOnly" : true, + "uniqueItems": true, + "description" : "Read-only collection of additional OrganizationDetails by various sources for this organization, ignored on create/update. These details must be created separately via the /udl/organizationdetails operations." + }, + "origNetwork" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 32, + "readOnly" : true, + "description" : "The originating source network on which this record was created, auto-populated by the system." + }, + "origin" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "description" : "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin." + }, + "source" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "description" : "Source of the data." + }, + "type" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 128, + "description" : "Type of organization (e.g. GOVERNMENT, CORPORATION, CONSORTIUM, ACADEMIC)." + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was last updated in the database, auto-populated by the system." + }, + "updatedBy" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who updated the row in the database, auto-populated by the system." + } + }, + "required" : ["classificationMarking", "dataMode", "name", "source", "type"], + }, + "OrganizationDetails" : { + "type" : "object", + "description" : "Model representation of additional detailed organization data as collected by a particular source.", + "properties" : { + "address1" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 120, + "description" : "Street number of the organization." + }, + "address2" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 120, + "description" : "Field for additional organization address information such as PO Box and unit number." + }, + "address3" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 120, + "description" : "Contains the third line of address information for an organization." + }, + "broker" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 128, + "description" : "Designated broker for this organization." + }, + "ceo" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 128, + "description" : "For organizations of type CORPORATION, the name of the Chief Executive Officer." + }, + "cfo" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 128, + "description" : "For organizations of type CORPORATION, the name of the Chief Financial Officer." + }, + "classificationMarking" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 128, + "description" : "Classification marking of the data in IC/CAPCO Portion-marked format." + }, + "createdAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was created in the database, auto-populated by the system." + }, + "createdBy" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who created the row in the database, auto-populated by the system." + }, + "cto" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 128, + "description" : "For organizations of type CORPORATION, the name of the Chief Technology Officer." + }, + "dataMode" : { + "$ref" : "#/definitions/OrganizationDetails_dataMode", + "minLength" : 1, + "maxLength" : 32, + "description" : "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n" + }, + "description" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 2147483647, + "description" : "Organization description." + }, + "ebitda" : { + "type" : "number", + "format" : "double", + "description" : "For organizations of type CORPORATION, the company EBITDA value as of financialYearEndDate in US Dollars." + }, + "email" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 320, + "description" : "Listed contact email address for the organization." + }, + "financialNotes" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 2147483647, + "description" : "For organizations of type CORPORATION, notes on company financials." + }, + "financialYearEndDate" : { + "type" : "string", + "format" : "date-time", + "description" : "For organizations of type CORPORATION, the effective financial year end date for revenue, EBITDA, and profit values." + }, + "fleetPlanNotes" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 2147483647, + "description" : "Satellite fleet planning notes for this organization." + }, + "formerOrgId" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "Former organization ID (if this organization previously existed as another organization)." + }, + "ftes" : { + "type" : "integer", + "format" : "int32", + "description" : "Total number of FTEs in this organization." + }, + "geoAdminLevel1" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 120, + "description" : "Administrative boundaries of the first sub-national level. Level 1 is simply the largest demarcation under whatever demarcation criteria has been determined by the governing body. For example, this may be a state or province." + }, + "geoAdminLevel2" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 120, + "description" : "Administrative boundaries of the second sub-national level. Level 2 is simply the second largest demarcation under whatever demarcation criteria has been determined by the governing body. For example, this may be a county or district." + }, + "geoAdminLevel3" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 120, + "description" : "Administrative boundaries of the third sub-national level. Level 3 is simply the third largest demarcation under whatever demarcation criteria has been determined by the governing body. For example, this may be a city or township." + }, + "id" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "Unique identifier of the record, auto-generated by the system." + }, + "idOrganization" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 36, + "description" : "Unique identifier of the parent organization." + }, + "massRanking" : { + "type" : "integer", + "format" : "int32", + "description" : "Mass ranking for this organization." + }, + "name" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 128, + "description" : "Organization details name." + }, + "origNetwork" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 32, + "readOnly" : true, + "description" : "The originating source network on which this record was created, auto-populated by the system." + }, + "origin" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "description" : "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin." + }, + "parentOrgId" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 36, + "description" : "Parent organization ID of this organization if it is a child organization." + }, + "postalCode" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 32, + "description" : "A postal code, such as PIN or ZIP Code, is a series of letters or digits or both included in the postal address of the organization." + }, + "profit" : { + "type" : "number", + "format" : "double", + "description" : "For organizations of type CORPORATION, total annual profit as of financialYearEndDate in US Dollars." + }, + "revenue" : { + "type" : "number", + "format" : "double", + "description" : "For organizations of type CORPORATION, total annual revenue as of financialYearEndDate in US Dollars." + }, + "revenueRanking" : { + "type" : "integer", + "format" : "int32", + "description" : "Revenue ranking for this organization." + }, + "riskManager" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 128, + "description" : "The name of the risk manager for the organization." + }, + "servicesNotes" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 2147483647, + "description" : "Notes on the services provided by the organization." + }, + "source" : { + "type" : "string", + "minLength" : 1, + "maxLength" : 64, + "description" : "Source of the data." + }, + "tags" : { + "type" : "array", "items" : {"type" : "string"}, + "description" : "Optional array of provider/source specific tags for this data, where each element is no longer than 32 characters, used for implementing data owner conditional access controls to restrict access to the data. Should be left null by data providers unless conditional access controls are coordinated with the UDL team." + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time", + "readOnly" : true, + "description" : "Time the row was last updated in the database, auto-populated by the system." + }, + "updatedBy" : { + "type" : "string", + "minLength" : 0, + "maxLength" : 64, + "readOnly" : true, + "description" : "Application user who updated the row in the database, auto-populated by the system." + } + }, + "required" : ["classificationMarking", "dataMode", "idOrganization", "name", "source"], + } + }, + "$ref" : "#/definitions/Antenna" +} diff --git a/tests/jsonschema_import/inputs/udl_openapi_aircraft_mission_tasking_subset.schema.json b/tests/jsonschema_import/inputs/udl_openapi_aircraft_mission_tasking_subset.schema.json new file mode 100644 index 00000000000..4be1c751971 --- /dev/null +++ b/tests/jsonschema_import/inputs/udl_openapi_aircraft_mission_tasking_subset.schema.json @@ -0,0 +1,391 @@ +{ + "components": { + "schemas": { + "AircraftMissionLocationTasking_Abridged": { + "description": "Collection of aircraft mission location information for this aircraft mission tasking.", + "properties": { + "airMsnPri": { + "description": "The code for the priority assigned to this mission.", + "example": "1A", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "alt": { + "description": "The altitude for this mission represented as hundreds of feet above MSL.", + "example": 210, + "format": "int32", + "type": "integer" + }, + "areaGeoRad": { + "description": "The radius of the circle around the location being reported in feet.", + "example": 1000, + "format": "int32", + "type": "integer" + }, + "endTime": { + "description": "The end time of this mission in ISO 8601 UTC format with millisecond precision.", + "example": "2021-10-25T12:00:00.123Z", + "format": "date-time", + "type": "string" + }, + "msnLocName": { + "description": "The name that identifies the location at which this mission is to be performed. This can be the name of a general target area, orbit, cap point, station, etc.", + "example": "KLSV", + "maxLength": 24, + "minLength": 0, + "type": "string" + }, + "msnLocPtBarT": { + "description": "The alpha-numeric specified location for this mission specified as a bearing angle in degrees relative to true north and a range in nautical miles (NM).", + "example": "330T-PT ALFA-50NM", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "msnLocPtLat": { + "description": "WGS-84 latitude of the mission location, in degrees. -90 to 90 degrees (negative values south of equator) for this tasked air mission.", + "example": 35.123, + "exclusiveMaximum": false, + "exclusiveMinimum": false, + "format": "double", + "maximum": 90, + "minimum": -90, + "type": "number" + }, + "msnLocPtLon": { + "description": "WGS-84 longitude of the mission location, in degrees. -180 to 180 degrees (negative values west of Prime Meridian) for this tasked air mission.", + "example": 79.01, + "exclusiveMaximum": false, + "exclusiveMinimum": false, + "format": "double", + "maximum": 180, + "minimum": -180, + "type": "number" + }, + "msnLocPtName": { + "description": "The location name for this mission.", + "example": "PT ALFA", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "startTime": { + "description": "The start time of this mission in ISO 8601 UTC format with millisecond precision.", + "example": "2021-10-25T12:00:00.123Z", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "startTime" + ], + "type": "object" + }, + "AircraftMissionTasking_Abridged": { + "description": "Collection that specifies the tasked country, tasked service, unit and mission level tasking for this ATO.", + "properties": { + "acMsnLocSeg": { + "description": "A collection of aircraft mission location information for this aircraft mission tasking.", + "items": { + "$ref": "#/components/schemas/AircraftMissionLocationTasking_Abridged" + }, + "type": "array" + }, + "alertStatus": { + "description": "The readiness status expressed in time (minutes) for an aircraft to be airborne after the launch order is received or the time required for a missile unit to assume battle stations.", + "example": 30, + "format": "int32", + "type": "integer" + }, + "amcMsnNum": { + "description": "The AMC number assigned to identify one aircraft from another.", + "example": "AMC:JJXD123HA045", + "maxLength": 16, + "minLength": 0, + "type": "string" + }, + "countryCode": { + "description": "The country code responsible for conducting this aircraft mission tasking for the exercise or operation.", + "example": "US", + "maxLength": 4, + "minLength": 1, + "type": "string" + }, + "depLocLat": { + "description": "WGS-84 latitude of the departure location, in degrees. -90 to 90 degrees (negative values south of equator) for this tasked air mission.", + "example": 35.123, + "exclusiveMaximum": false, + "exclusiveMinimum": false, + "format": "double", + "maximum": 90, + "minimum": -90, + "type": "number" + }, + "depLocLon": { + "description": "WGS-84 longitude of the departure location, in degrees. -180 to 180 degrees (negative values west of Prime Meridian) for this tasked air mission.", + "example": 79.2354, + "exclusiveMaximum": false, + "exclusiveMinimum": false, + "format": "double", + "maximum": 180, + "minimum": -180, + "type": "number" + }, + "depLocName": { + "description": "The location or name specified for the departure of the tasked air mission.", + "example": "ICAO:KBIF", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "depLocUTM": { + "description": "The departure location specified in UTM (100 meter) coordinates for the tasked air mission.", + "example": "32WDL123123", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "depTime": { + "description": "The time of departure for the tasked air mission in ISO8601 UTC format with millisecond precision.", + "example": "2021-10-25T12:00:00.123Z", + "format": "date-time", + "type": "string" + }, + "indACTasking": { + "description": "A collection of the individual aircraft assigned to this aircraft mission tasking.", + "items": { + "$ref": "#/components/schemas/IndividualAircraftTasking_Abridged" + }, + "type": "array" + }, + "msnCommander": { + "description": "The commander responsible for the planning and execution of the forces necessary to achieve desired objectives.", + "example": "MC", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "msnNum": { + "description": "The mission number assigned to this mission.", + "example": "D123HA", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "pkgId": { + "description": "The identifier for the composite set of missions for this operation/exercise.", + "example": "ZZ", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "priMsnType": { + "description": "The code for the preferred type or designator for a tasked air mission.", + "example": "CAS", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "rcvyLocLat": { + "description": "An array of WGS-84 latitude of the recovery locations, in degrees. -90 to 90 degrees (negative values south of equator) for this tasked air mission.", + "example": [ + 48.8584, + 40.7554 + ], + "items": { + "format": "double", + "type": "number" + }, + "type": "array" + }, + "rcvyLocLon": { + "description": "An array of WGS-84 longitude of the recovery locations, in degrees. -180 to 180 degrees (negative values west of Prime Meridian) for this tasked air mission.", + "example": [ + 2.2945, + -73.9866 + ], + "items": { + "format": "double", + "type": "number" + }, + "type": "array" + }, + "rcvyLocName": { + "description": "An array of locations specified for the recovery of the tasked air mission represented by varying formats.", + "example": [ + "ARRLOC:KBIF", + "ARRLOC:KDZ7" + ], + "items": { + "example": "[\"ARRLOC:KBIF\",\"ARRLOC:KDZ7\"]", + "type": "string" + }, + "maxItems": 36, + "minItems": 0, + "type": "array" + }, + "rcvyLocUTM": { + "description": "An array of recovery locations specified in UTM (100 meter) coordinates for the tasked air mission.", + "example": [ + "ARRUTMO:32WDL123123", + "ARRUTMO:32WDL321321" + ], + "items": { + "example": "[\"ARRUTMO:32WDL123123\",\"ARRUTMO:32WDL321321\"]", + "type": "string" + }, + "maxItems": 36, + "minItems": 0, + "type": "array" + }, + "rcvyTime": { + "description": "An array of recovery times for the tasked air mission in ISO8601 UTC format with millisecond precision.", + "example": [ + "2021-10-25T16:00:00.234Z", + "2021-10-26T16:00:00.234Z" + ], + "items": { + "format": "date-time", + "type": "string" + }, + "type": "array" + }, + "resMsnInd": { + "description": "An indicator of whether a mission is or will be a residual mission.", + "example": "N", + "maxLength": 1, + "minLength": 0, + "type": "string" + }, + "secMsnType": { + "description": "The code for the alternative type of a tasked air mission.", + "example": "SEAD", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "taskedService": { + "description": "The service tasked with conducting this aircraft mission tasking for the exercise or operation.", + "example": "A", + "maxLength": 1, + "minLength": 1, + "type": "string" + }, + "unitDesignator": { + "description": "The designator of the unit that is tasked to perform this aircraft mission tasking.", + "example": "AMPHIB5DIV", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "unitLocName": { + "description": "The tasked units location expressed as an ICAO or a place name.", + "example": "ICAO:KXXQ", + "maxLength": 36, + "minLength": 0, + "type": "string" + } + }, + "required": [ + "countryCode", + "taskedService", + "unitDesignator" + ], + "type": "object" + }, + "IndividualAircraftTasking_Abridged": { + "description": "Collection that specifies the naval flight operations for this ATO.", + "properties": { + "acftType": { + "description": "The type and model number for the aircraft. The field may specify a value of an aircraft not yet assigned an aircraft code contained in the aircraft codes list.", + "example": "F35A", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "callSign": { + "description": "The call sign assigned to this mission aircraft.", + "example": "EAGLE47", + "maxLength": 24, + "minLength": 0, + "type": "string" + }, + "iffSifMode1Code": { + "description": "The mode 1 and code of the Identification Friend or FOE (IFF) or Selective Identification Feature (SIF).", + "example": "111", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "iffSifMode2Code": { + "description": "The mode 2 and code of the Identification Friend or FOE (IFF) or Selective Identification Feature (SIF).", + "example": "20147", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "iffSifMode3Code": { + "description": "The mode 3 and code of the Identification Friend or FOE (IFF) or Selective Identification Feature (SIF).", + "example": "30147", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "juAddress": { + "description": "An optional array of link 16 octal track numbers assigned as the primary JTIDS Unit (JU) address for the mission aircraft.", + "example": 12345, + "items": { + "example": 12345, + "format": "int32", + "type": "integer" + }, + "type": "array" + }, + "link16CallSign": { + "description": "The Link 16 abbreviated call sign assigned to the ACA. This is normally the first and last letter and the last two numbers of the call sign.", + "example": "EE47", + "maxLength": 8, + "minLength": 0, + "type": "string" + }, + "numAcft": { + "description": "The number of aircraft participating in this mission.", + "example": 2, + "format": "int32", + "type": "integer" + }, + "priConfigCode": { + "description": "The code that indicates the ordinance mix carried on this mission aircraft.", + "example": "6A2W3", + "maxLength": 48, + "minLength": 0, + "type": "string" + }, + "secConfigCode": { + "description": "The code for the secondary ordinance mix carried on this mission aircraft.", + "example": "2S2WG", + "maxLength": 48, + "minLength": 0, + "type": "string" + }, + "tacanChan": { + "description": "The TACAN channel assigned to this mission aircraft.", + "example": 123, + "format": "int32", + "type": "integer" + } + }, + "required": [ + "acftType" + ], + "type": "object" + } + } + }, + "info": { + "title": "Unified Data Library Services API", + "version": "1.28.0-MocktailMadness" + }, + "openapi": "3.0.1" +} diff --git a/tests/jsonschema_import/inputs/udl_openapi_ais_subset.schema.json b/tests/jsonschema_import/inputs/udl_openapi_ais_subset.schema.json new file mode 100644 index 00000000000..80ff47587f6 --- /dev/null +++ b/tests/jsonschema_import/inputs/udl_openapi_ais_subset.schema.json @@ -0,0 +1,375 @@ +{ + "components": { + "schemas": { + "AIS_Abridged": { + "description": "Self-reported information obtained from Automatic Identification System (AIS) equipment. This contains information such as unique identification, status, position, course, and speed. The AIS is an automatic tracking system that uses transceivers on ships and is used by vessel traffic services. Although technically and operationally distinct, the AIS system is analogous to ADS-B that performs a similar function for aircraft. AIS is intended to assist a vessel's watchstanding officers and allow maritime authorities to track and monitor vessel movements. AIS integrates a standardized VHF transceiver with a positioning system such as Global Positioning System receiver, with other electronic navigation sensors, such as gyrocompass or rate of turn indicator. Vessels fitted with AIS transceivers can be tracked by AIS base stations located along coast lines or, when out of range of terrestrial networks, through a growing number of satellites that are fitted with special AIS receivers which are capable of deconflicting a large number of signatures.", + "properties": { + "antennaRefDimensions": { + "description": "The reference dimensions of the vessel, reported as [A, B, C, D], in meters. Where the array values represent the distance fore (A), aft (B), to port (C), and to starboard (D) of the navigation antenna. Array with values A = C = 0 and B, D > 0 indicate the length (B) and width (D) of the vessel without antenna position reference.", + "example": [ + 50.1, + 50.1, + 20.1, + 20.1 + ], + "items": { + "format": "double", + "type": "number" + }, + "type": "array" + }, + "avgSpeed": { + "description": "The average speed, in kilometers/hour, calculated for the subject vessel during the latest voyage (port to port).", + "example": 12.1, + "format": "double", + "type": "number" + }, + "callSign": { + "description": "A uniquely designated identifier for the vessel's transmitter station.", + "example": "V2OZ", + "maxLength": 24, + "minLength": 0, + "type": "string" + }, + "cargoType": { + "description": "The reported cargo type. Intended as, but not constrained to, the USCG NAVCEN AIS cargo definitions. Users should refer to USCG Navigation Center documentation for specific definitions associated with ship and cargo types. USCG NAVCEN documentation may be found at https://www.navcen.uscg.gov.", + "example": "Freight", + "maxLength": 48, + "minLength": 0, + "type": "string" + }, + "classificationMarking": { + "description": "Classification marking of the data in IC/CAPCO Portion-marked format.", + "example": "U", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "course": { + "description": "The course-over-ground reported by the vessel, in degrees.", + "example": 157.1, + "format": "double", + "type": "number" + }, + "createdAt": { + "description": "Time the row was created in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "createdBy": { + "description": "Application user who created the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "currentPortGUID": { + "description": "The US Geographic Unique Identifier of the current port hosting the vessel.", + "example": "0ABC", + "maxLength": 4, + "minLength": 0, + "type": "string" + }, + "currentPortLOCODE": { + "description": "The UN Location Code of the current port hosting the vessel.", + "example": "XF013", + "maxLength": 5, + "minLength": 0, + "type": "string" + }, + "dataMode": { + "description": "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n", + "enum": [ + "REAL", + "TEST", + "SIMULATED", + "EXERCISE" + ], + "example": "TEST", + "maxLength": 32, + "minLength": 1, + "type": "string" + }, + "destination": { + "description": "The destination of the vessel according to the AIS transmission.", + "example": "USCLE", + "maxLength": 20, + "minLength": 0, + "type": "string" + }, + "destinationETA": { + "description": "The Estimated Time of Arrival of the vessel at the destination, in ISO 8601 UTC format.", + "example": "2021-02-25T12:00:00.123456Z", + "format": "date-time", + "type": "string" + }, + "distanceToGo": { + "description": "The remaining distance, in kilometers, for the vessel to reach the reported destination.", + "example": 150.5, + "format": "double", + "type": "number" + }, + "distanceTravelled": { + "description": "The distance, in kilometers, that the vessel has travelled since departing the last port.", + "example": 200.3, + "format": "double", + "type": "number" + }, + "draught": { + "description": "The maximum static draught, in meters, of the vessel according to the AIS transmission.", + "example": 21.1, + "format": "double", + "type": "number" + }, + "engagedIn": { + "description": "The activity that the vessel is engaged in. This entry applies only when the shipType = Other.", + "example": "Cargo", + "maxLength": 48, + "minLength": 0, + "type": "string" + }, + "etaCalculated": { + "description": "The Estimated Time of Arrival of the vessel at the destination port, according to MarineTraffic calculations, in ISO 8601 UTC format.", + "example": "2021-02-25T12:00:00.123456Z", + "format": "date-time", + "type": "string" + }, + "etaUpdated": { + "description": "The date and time that the ETA was calculated by MarineTraffic, in ISO 8601 UTC format.", + "example": "2021-02-25T12:00:00.123456Z", + "format": "date-time", + "type": "string" + }, + "id": { + "description": "Unique identifier of the record, auto-generated by the system.", + "example": "AIS-ID", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "idTrack": { + "description": "Unique identifier of the Track.", + "example": "TRACK-ID", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "idVessel": { + "description": "Unique identifier of the vessel.", + "example": "VESSEL-ID", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "imon": { + "description": "The International Maritime Organization Number of the vessel. IMON is a seven-digit number that uniquely identifies the vessel.", + "example": 9015462, + "format": "int64", + "type": "integer" + }, + "lastPortGUID": { + "description": "The US Geographic Unique Identifier of the last port visited by the vessel.", + "example": "0VAX", + "maxLength": 4, + "minLength": 0, + "type": "string" + }, + "lastPortLOCODE": { + "description": "The UN Location Code of the last port visited by the vessel.", + "example": "USSKY", + "maxLength": 5, + "minLength": 0, + "type": "string" + }, + "lat": { + "description": "WGS-84 latitude of the vessel position, in degrees. -90 to 90 degrees (negative values south of equator).", + "example": 47.758499, + "format": "double", + "maximum": 90, + "minimum": -90, + "type": "number" + }, + "length": { + "description": "The overall length of the vessel, in meters. A value of 511 indicates a vessel length of 511 meters or greater.", + "example": 511.1, + "format": "double", + "type": "number" + }, + "lon": { + "description": "WGS-84 longitude of the vessel position, in degrees. -180 to 180 degrees (negative values west of Prime Meridian).", + "example": -5.154223, + "format": "double", + "maximum": 180, + "minimum": -180, + "type": "number" + }, + "maxSpeed": { + "description": "The maximum speed, in kilometers/hour, reported by the subject vessel during the latest voyage (port to port).", + "example": 13.3, + "format": "double", + "type": "number" + }, + "mmsi": { + "description": "The Maritime Mobile Service Identity of the vessel. MMSI is a nine-digit number that identifies the transmitter station of the vessel.", + "example": 304010417, + "format": "int64", + "type": "integer" + }, + "navStatus": { + "description": "The AIS Navigational Status of the vessel (e.g. Underway Using Engine, Moored, Aground, etc.). Intended as, but not constrained to, the USCG NAVCEN navigation status definitions. Users should refer to USCG Navigation Center documentation for specific definitions associated with navigation status. USCG NAVCEN documentation may be found at https://www.navcen.uscg.gov.", + "example": "Underway Using Engine", + "maxLength": 64, + "minLength": 0, + "type": "string" + }, + "nextPortGUID": { + "description": "The US Geographic Unique Identifier of the next destination port of the vessel.", + "example": "0Z8Q", + "maxLength": 4, + "minLength": 0, + "type": "string" + }, + "nextPortLOCODE": { + "description": "The UN Location Code of the next destination port of the vessel.", + "example": "USCLE", + "maxLength": 5, + "minLength": 0, + "type": "string" + }, + "origNetwork": { + "description": "The originating source network on which this record was created, auto-populated by the system.", + "example": "ORIG", + "maxLength": 32, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "origin": { + "description": "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin.", + "example": "THIRD_PARTY_DATASOURCE", + "maxLength": 64, + "minLength": 0, + "type": "string" + }, + "posDeviceType": { + "description": "The type of electronic position fixing device (e.g. GPS, GLONASS, etc.). Intended as, but not constrained to, the USCG NAVCEN electronic position fixing device definitions. Users should refer to USCG Navigation Center documentation for specific device type information. USCG NAVCEN documentation may be found at https://www.navcen.uscg.gov.", + "example": "GPS", + "maxLength": 24, + "minLength": 0, + "type": "string" + }, + "posHiAccuracy": { + "description": "Flag indicating high reported position accuracy (less than or equal to 10 meters). A value of 0/false indicates low accuracy (greater than 10 meters).", + "example": true, + "type": "boolean" + }, + "posHiLatency": { + "description": "Flag indicating high reported position latency (greater than 5 seconds). A value of 0/false indicates low latency (less than 5 seconds).", + "example": true, + "type": "boolean" + }, + "rateOfTurn": { + "description": "The Rate-of-Turn for the vessel, in degrees/minute. Positive value indicates that the vessel is turning right.", + "example": 22.1, + "format": "double", + "type": "number" + }, + "shipDescription": { + "description": "Further description or explanation of the vessel or type.", + "example": "Search and rescue vessels", + "maxLength": 100, + "minLength": 0, + "type": "string" + }, + "shipName": { + "description": "The name of the vessel. Vessel names that exceed the AIS 20 character are shortened (not truncated) to 15 character-spaces, followed by an underscore and the last 4 characters-spaces of the vessel full name.", + "example": "DORNUM", + "maxLength": 24, + "minLength": 0, + "type": "string" + }, + "shipType": { + "description": "The reported ship type (e.g. Passenger, Tanker, Cargo, Other, etc.). See the engagedIn and specialCraft entries for additional information on certain types of vessels.", + "example": "Passenger", + "maxLength": 48, + "minLength": 0, + "type": "string" + }, + "source": { + "description": "Source of the data.", + "example": "Bluestaq", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "sourceDL": { + "description": "The source data library from which this record was received. This could be a remote or tactical UDL or another data library. If null, the record should be assumed to have originated from the primary Enterprise UDL.", + "example": "AXE", + "maxLength": 64, + "minLength": 0, + "readOnly": true, + "type": "string" + }, + "specialCraft": { + "description": "The type of special craft designation of the vessel. This entry applies only when the shipType = Special Craft.", + "example": "Tug", + "maxLength": 48, + "minLength": 0, + "type": "string" + }, + "specialManeuver": { + "description": "Flag indicating that the vessel is engaged in a special maneuver (e.g. Waterway Navigation).", + "example": false, + "type": "boolean" + }, + "speed": { + "description": "The speed-over-ground reported by the vessel, in kilometers/hour.", + "example": 10.5, + "format": "double", + "type": "number" + }, + "trueHeading": { + "description": "The true heading reported by the vessel, in degrees.", + "example": 329.1, + "format": "double", + "type": "number" + }, + "ts": { + "description": "The timestamp that the vessel position was recorded, in ISO 8601 UTC format.", + "example": "2021-02-25T12:00:00.123456Z", + "format": "date-time", + "type": "string" + }, + "vesselFlag": { + "description": "The flag of the subject vessel according to AIS transmission.", + "example": "United States", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "width": { + "description": "The breadth of the vessel, in meters. A value of 63 indicates a vessel breadth of 63 meters or greater.", + "example": 24.1, + "format": "double", + "type": "number" + } + }, + "required": [ + "classificationMarking", + "dataMode", + "source", + "ts" + ], + "type": "object" + } + } + }, + "info": { + "title": "Unified Data Library Services API", + "version": "1.28.0-MocktailMadness" + }, + "openapi": "3.0.1" +} diff --git a/tests/jsonschema_import/inputs/udl_openapi_antenna_subset.schema.json b/tests/jsonschema_import/inputs/udl_openapi_antenna_subset.schema.json new file mode 100644 index 00000000000..27e8e226ba8 --- /dev/null +++ b/tests/jsonschema_import/inputs/udl_openapi_antenna_subset.schema.json @@ -0,0 +1,778 @@ +{ + "components": { + "schemas": { + "Antenna": { + "description": "Model representation of information on on-orbit/spacecraft communication antennas. A spacecraft may have multiple antennas and each antenna can have multiple 'details' records compiled by different sources.", + "properties": { + "antennaDetails": { + "description": "Read-only collection of additional AntennaDetails by various sources for this organization, ignored on create/update. These details must be created separately via the /udl/antennadetails operations.", + "items": { + "$ref": "#/components/schemas/AntennaDetails" + }, + "readOnly": true, + "type": "array", + "uniqueItems": true + }, + "createdAt": { + "description": "Time the row was created in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "createdBy": { + "description": "Application user who created the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "dataMode": { + "description": "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n", + "enum": [ + "REAL", + "TEST", + "SIMULATED", + "EXERCISE" + ], + "example": "TEST", + "maxLength": 32, + "minLength": 1, + "type": "string" + }, + "id": { + "description": "Unique identifier of the record, auto-generated by the system.", + "example": "ANTENNA-ID", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "name": { + "description": "Antenna name.", + "example": "IRIDIUM NEXT 121-ANTENNA-10075", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "origNetwork": { + "description": "The originating source network on which this record was created, auto-populated by the system.", + "example": "ORIG", + "maxLength": 32, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "origin": { + "description": "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin.", + "example": "THIRD_PARTY_DATASOURCE", + "maxLength": 64, + "minLength": 0, + "type": "string" + }, + "source": { + "description": "Source of the data.", + "example": "Bluestaq", + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "updatedAt": { + "description": "Time the row was last updated in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "updatedBy": { + "description": "Application user who updated the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 0, + "readOnly": true, + "type": "string" + } + }, + "readOnly": true, + "required": [ + "dataMode", + "name", + "source" + ], + "type": "object" + }, + "AntennaDetails": { + "description": "Detailed information for a spacecraft communication antenna. One antenna may have multiple AntennaDetails records, compiled by various sources.", + "properties": { + "beamForming": { + "description": "Boolean indicating if this is a beam forming antenna.", + "example": false, + "type": "boolean" + }, + "beamwidth": { + "description": "Array of angles between the half-power (-3 dB) points of the main lobe of the antenna, in degrees.", + "example": 14.1, + "format": "double", + "type": "number" + }, + "classificationMarking": { + "description": "Classification marking of the data in IC/CAPCO Portion-marked format.", + "example": "U", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "createdAt": { + "description": "Time the row was created in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "createdBy": { + "description": "Application user who created the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "dataMode": { + "description": "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n", + "enum": [ + "REAL", + "TEST", + "SIMULATED", + "EXERCISE" + ], + "example": "TEST", + "maxLength": 32, + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Antenna description.", + "example": "Description of antenna A", + "maxLength": 512, + "minLength": 0, + "type": "string" + }, + "diameter": { + "description": "Antenna diameter in meters.", + "example": 0.01, + "format": "double", + "type": "number" + }, + "endFrequency": { + "description": "Antenna end of frequency range in Mhz.", + "example": 3.3, + "format": "double", + "type": "number" + }, + "gain": { + "description": "Antenna maximum gain in dBi.", + "example": 20.1, + "format": "double", + "type": "number" + }, + "gainTolerance": { + "description": "Antenna gain tolerance in dB.", + "example": 5.1, + "format": "double", + "type": "number" + }, + "id": { + "description": "Unique identifier of the record, auto-generated by the system.", + "example": "ANTENNADETAILS-ID", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "idAntenna": { + "description": "Unique identifier of the parent Antenna.", + "example": "ANTENNA-ID", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "manufacturerOrg": { + "$ref": "#/components/schemas/Organization" + }, + "manufacturerOrgId": { + "description": "ID of the organization that manufactures the antenna.", + "example": "MANUFACTUREORG-ID", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "mode": { + "description": "Antenna mode (e.g. TX,RX).", + "enum": [ + "TX", + "RX" + ], + "example": "TX", + "maxLength": 4, + "minLength": 0, + "type": "string" + }, + "origNetwork": { + "description": "The originating source network on which this record was created, auto-populated by the system.", + "example": "ORIG", + "maxLength": 32, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "origin": { + "description": "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin.", + "example": "THIRD_PARTY_DATASOURCE", + "maxLength": 64, + "minLength": 0, + "type": "string" + }, + "polarization": { + "description": "Antenna polarization in degrees.", + "example": 45.1, + "format": "double", + "type": "number" + }, + "position": { + "description": "Antenna position (e.g. Top, Nadir, Side).", + "example": "Top", + "maxLength": 128, + "minLength": 0, + "type": "string" + }, + "size": { + "description": "Array with 1-2 values specifying the length and width (for rectangular) and just length for dipole antennas in meters.", + "example": [ + 0.03, + 0.05 + ], + "items": { + "format": "double", + "type": "number" + }, + "type": "array" + }, + "source": { + "description": "Source of the data.", + "example": "Bluestaq", + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "startFrequency": { + "description": "Antenna start of frequency range in Mhz.", + "example": 2.1, + "format": "double", + "type": "number" + }, + "steerable": { + "description": "Boolean indicating if this antenna is steerable.", + "example": false, + "type": "boolean" + }, + "tags": { + "description": "Optional array of provider/source specific tags for this data, where each element is no longer than 32 characters, used for implementing data owner conditional access controls to restrict access to the data. Should be left null by data providers unless conditional access controls are coordinated with the UDL team.", + "example": [ + "PROVIDER_TAG1", + "PROVIDER_TAG2" + ], + "items": { + "example": "[\"PROVIDER_TAG1\",\"PROVIDER_TAG2\"]", + "type": "string" + }, + "type": "array" + }, + "type": { + "description": "Type of antenna (e.g. Reflector, Double Reflector, Shaped Reflector, Horn, Parabolic, etc.).", + "example": "Reflector", + "maxLength": 64, + "minLength": 0, + "type": "string" + }, + "updatedAt": { + "description": "Time the row was last updated in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "updatedBy": { + "description": "Application user who updated the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 0, + "readOnly": true, + "type": "string" + } + }, + "readOnly": true, + "required": [ + "classificationMarking", + "dataMode", + "idAntenna", + "source" + ], + "type": "object" + }, + "Organization": { + "description": "An organization such as a corporation, manufacturer, consortium, government, etc. An organization may have parent and child organizations as well as link to a former organization if this org previously existed as another organization.", + "properties": { + "active": { + "description": "Boolean indicating if this organization is currently active.", + "example": false, + "type": "boolean" + }, + "category": { + "description": "Subtype or category of the organization (e.g. Private company, stock market quoted company, subsidiary, goverment department/agency, etc).", + "example": "Private company", + "maxLength": 128, + "minLength": 0, + "type": "string" + }, + "classificationMarking": { + "description": "Classification marking of the data in IC/CAPCO Portion-marked format.", + "example": "U", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "countryCode": { + "description": "Country of the physical location of the organization. This value is typically the ISO 3166 Alpha-2 two-character country code. However, it can also represent various consortiums that do not appear in the ISO document. The code must correspond to an existing country in the UDL\u2019s country API. Call udl/country/{code} to get any associated FIPS code, ISO Alpha-3 code, or alternate code values that exist for the specified country code.", + "example": "US", + "maxLength": 4, + "minLength": 0, + "type": "string" + }, + "createdAt": { + "description": "Time the row was created in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "createdBy": { + "description": "Application user who created the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "dataMode": { + "description": "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n", + "enum": [ + "REAL", + "TEST", + "SIMULATED", + "EXERCISE" + ], + "example": "TEST", + "maxLength": 32, + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Organization description.", + "example": "Example description", + "maxLength": 256, + "minLength": 0, + "type": "string" + }, + "externalId": { + "description": "Optional externally provided identifier for this row.", + "example": "EXTERNAL-ID", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "id": { + "description": "Unique identifier of the record, auto-generated by the system.", + "example": "ORGANIZATION-ID", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "name": { + "description": "Organization name.", + "example": "some.user", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "nationality": { + "description": "Country of registration or ownership of the organization. This value is typically the ISO 3166 Alpha-2 two-character country code, however it can also represent various consortiums that do not appear in the ISO document. The code must correspond to an existing country in the UDL\u2019s country API. Call udl/country/{code} to get any associated FIPS code, ISO Alpha-3 code, or alternate code values that exist for the specified country code.", + "example": "US", + "maxLength": 4, + "minLength": 0, + "type": "string" + }, + "organizationDetails": { + "description": "Read-only collection of additional OrganizationDetails by various sources for this organization, ignored on create/update. These details must be created separately via the /udl/organizationdetails operations.", + "items": { + "$ref": "#/components/schemas/OrganizationDetails" + }, + "readOnly": true, + "type": "array", + "uniqueItems": true + }, + "origNetwork": { + "description": "The originating source network on which this record was created, auto-populated by the system.", + "example": "OPS1", + "maxLength": 32, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "origin": { + "description": "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin.", + "example": "some.user", + "maxLength": 64, + "minLength": 0, + "type": "string" + }, + "source": { + "description": "Source of the data.", + "example": "some.user", + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Type of organization (e.g. GOVERNMENT, CORPORATION, CONSORTIUM, ACADEMIC).", + "example": "GOVERNMENT", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "updatedAt": { + "description": "Time the row was last updated in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "updatedBy": { + "description": "Application user who updated the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 0, + "readOnly": true, + "type": "string" + } + }, + "readOnly": true, + "required": [ + "classificationMarking", + "dataMode", + "name", + "source", + "type" + ], + "type": "object" + }, + "OrganizationDetails": { + "description": "Model representation of additional detailed organization data as collected by a particular source.", + "properties": { + "address1": { + "description": "Street number of the organization.", + "example": "123 Main Street", + "maxLength": 120, + "minLength": 0, + "type": "string" + }, + "address2": { + "description": "Field for additional organization address information such as PO Box and unit number.", + "example": "Apt 4B", + "maxLength": 120, + "minLength": 0, + "type": "string" + }, + "address3": { + "description": "Contains the third line of address information for an organization.", + "example": "Colorado Springs CO, 80903", + "maxLength": 120, + "minLength": 0, + "type": "string" + }, + "broker": { + "description": "Designated broker for this organization.", + "example": "some.user", + "maxLength": 128, + "minLength": 0, + "type": "string" + }, + "ceo": { + "description": "For organizations of type CORPORATION, the name of the Chief Executive Officer.", + "example": "some.user", + "maxLength": 128, + "minLength": 0, + "type": "string" + }, + "cfo": { + "description": "For organizations of type CORPORATION, the name of the Chief Financial Officer.", + "example": "some.user", + "maxLength": 128, + "minLength": 0, + "type": "string" + }, + "classificationMarking": { + "description": "Classification marking of the data in IC/CAPCO Portion-marked format.", + "example": "U", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "createdAt": { + "description": "Time the row was created in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "createdBy": { + "description": "Application user who created the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "cto": { + "description": "For organizations of type CORPORATION, the name of the Chief Technology Officer.", + "example": "some.user", + "maxLength": 128, + "minLength": 0, + "type": "string" + }, + "dataMode": { + "description": "Indicator of whether the data is EXERCISE, REAL, SIMULATED, or TEST data:\n\nEXERCISE: Data pertaining to a government or military exercise. The data may include both real and simulated data.\n\nREAL: Data collected or produced that pertains to real-world objects, events, and analysis.\n\nSIMULATED: Synthetic data generated by a model to mimic real-world datasets.\n\nTEST: Specific datasets used to evaluate compliance with specifications and requirements, and for validating technical, functional, and performance characteristics.\n\n", + "enum": [ + "REAL", + "TEST", + "SIMULATED", + "EXERCISE" + ], + "example": "TEST", + "maxLength": 32, + "minLength": 1, + "type": "string" + }, + "description": { + "description": "Organization description.", + "example": "Example description", + "maxLength": 2147483647, + "minLength": 0, + "type": "string" + }, + "ebitda": { + "description": "For organizations of type CORPORATION, the company EBITDA value as of financialYearEndDate in US Dollars.", + "example": 123.4, + "format": "double", + "type": "number" + }, + "email": { + "description": "Listed contact email address for the organization.", + "example": "some_organization@organization.com", + "maxLength": 320, + "minLength": 0, + "type": "string" + }, + "financialNotes": { + "description": "For organizations of type CORPORATION, notes on company financials.", + "example": "Example notes", + "maxLength": 2147483647, + "minLength": 0, + "type": "string" + }, + "financialYearEndDate": { + "description": "For organizations of type CORPORATION, the effective financial year end date for revenue, EBITDA, and profit values.", + "example": "2021-01-01T01:01:01.123Z", + "format": "date-time", + "type": "string" + }, + "fleetPlanNotes": { + "description": "Satellite fleet planning notes for this organization.", + "example": "Example notes", + "maxLength": 2147483647, + "minLength": 0, + "type": "string" + }, + "formerOrgId": { + "description": "Former organization ID (if this organization previously existed as another organization).", + "example": "FORMERORG-ID", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "ftes": { + "description": "Total number of FTEs in this organization.", + "example": 123, + "format": "int32", + "type": "integer" + }, + "geoAdminLevel1": { + "description": "Administrative boundaries of the first sub-national level. Level 1 is simply the largest demarcation under whatever demarcation criteria has been determined by the governing body. For example, this may be a state or province.", + "example": "Colorado", + "maxLength": 120, + "minLength": 0, + "type": "string" + }, + "geoAdminLevel2": { + "description": "Administrative boundaries of the second sub-national level. Level 2 is simply the second largest demarcation under whatever demarcation criteria has been determined by the governing body. For example, this may be a county or district.", + "example": "El Paso County", + "maxLength": 120, + "minLength": 0, + "type": "string" + }, + "geoAdminLevel3": { + "description": "Administrative boundaries of the third sub-national level. Level 3 is simply the third largest demarcation under whatever demarcation criteria has been determined by the governing body. For example, this may be a city or township.", + "example": "Colorado Springs", + "maxLength": 120, + "minLength": 0, + "type": "string" + }, + "id": { + "description": "Unique identifier of the record, auto-generated by the system.", + "example": "ORGANIZATIONDETAILS-ID", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "idOrganization": { + "description": "Unique identifier of the parent organization.", + "example": "ORGANIZATION-ID", + "maxLength": 36, + "minLength": 1, + "type": "string" + }, + "massRanking": { + "description": "Mass ranking for this organization.", + "example": 123, + "format": "int32", + "type": "integer" + }, + "name": { + "description": "Organization details name.", + "example": "some.user", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "origNetwork": { + "description": "The originating source network on which this record was created, auto-populated by the system.", + "example": "OPS1", + "maxLength": 32, + "minLength": 1, + "readOnly": true, + "type": "string" + }, + "origin": { + "description": "Originating system or organization which produced the data, if different from the source. The origin may be different than the source if the source was a mediating system which forwarded the data on behalf of the origin system. If null, the source may be assumed to be the origin.", + "example": "some.user", + "maxLength": 64, + "minLength": 0, + "type": "string" + }, + "parentOrgId": { + "description": "Parent organization ID of this organization if it is a child organization.", + "example": "PARENTORG-ID", + "maxLength": 36, + "minLength": 0, + "type": "string" + }, + "postalCode": { + "description": "A postal code, such as PIN or ZIP Code, is a series of letters or digits or both included in the postal address of the organization.", + "example": "80903", + "maxLength": 32, + "minLength": 0, + "type": "string" + }, + "profit": { + "description": "For organizations of type CORPORATION, total annual profit as of financialYearEndDate in US Dollars.", + "example": 123.4, + "format": "double", + "type": "number" + }, + "revenue": { + "description": "For organizations of type CORPORATION, total annual revenue as of financialYearEndDate in US Dollars.", + "example": 123.4, + "format": "double", + "type": "number" + }, + "revenueRanking": { + "description": "Revenue ranking for this organization.", + "example": 123, + "format": "int32", + "type": "integer" + }, + "riskManager": { + "description": "The name of the risk manager for the organization.", + "example": "some.user", + "maxLength": 128, + "minLength": 0, + "type": "string" + }, + "servicesNotes": { + "description": "Notes on the services provided by the organization.", + "example": "Example notes", + "maxLength": 2147483647, + "minLength": 0, + "type": "string" + }, + "source": { + "description": "Source of the data.", + "example": "some.user", + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "tags": { + "description": "Optional array of provider/source specific tags for this data, where each element is no longer than 32 characters, used for implementing data owner conditional access controls to restrict access to the data. Should be left null by data providers unless conditional access controls are coordinated with the UDL team.", + "example": [ + "PROVIDER_TAG1", + "PROVIDER_TAG2" + ], + "items": { + "example": "[\"PROVIDER_TAG1\",\"PROVIDER_TAG2\"]", + "type": "string" + }, + "type": "array" + }, + "updatedAt": { + "description": "Time the row was last updated in the database, auto-populated by the system.", + "example": "2018-01-01T16:00:00.123Z", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "updatedBy": { + "description": "Application user who updated the row in the database, auto-populated by the system.", + "example": "some.user", + "maxLength": 64, + "minLength": 0, + "readOnly": true, + "type": "string" + } + }, + "readOnly": true, + "required": [ + "classificationMarking", + "dataMode", + "idOrganization", + "name", + "source" + ], + "type": "object" + } + } + }, + "info": { + "title": "Unified Data Library Services API", + "version": "1.28.0-MocktailMadness" + }, + "openapi": "3.0.1" +} From 0c30f15f37394ebf9dad3c23a0e4784df8ee4ba7 Mon Sep 17 00:00:00 2001 From: TJKoury Date: Thu, 18 Dec 2025 16:02:59 -0500 Subject: [PATCH 5/5] update documentation --- README.md | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/README.md b/README.md index 19a9bc580d7..4ba82889540 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,130 @@ - JSON Schema schema import/export (`--jsonschema`, `*.schema.json`) - Optional lossless JSON Schema round-tripping via `--jsonschema-xflatbuffers` metadata +## Fork Features + +This fork adds a few features to the `flatc` compiler intended to treat JSON Schema as a first-class schema format, while still flowing through the same FlatBuffers schema IR (see [`reflection/reflection.fbs`](reflection/reflection.fbs)). + +### Preserve-case naming (`--preserve-case`) + +By default, many language generators apply case conversions to schema identifiers (for example converting `snake_case` field names into `camelCase` accessors). The `--preserve-case` flag disables this name mangling for identifiers coming from the schema, and emits names “as written” instead. + +Example: + +```sh +flatc --cpp --preserve-case schema.fbs +``` + +Notes: + +- This is currently supported for these generators: C++, Go, Java, Rust, Dart, Python, TypeScript, PHP, and JSON Schema (see [`src/flatc.cpp`](src/flatc.cpp)). +- Implementation: [`src/flatc.cpp`](src/flatc.cpp), [`src/util.cpp`](src/util.cpp). +- Tests: [`tests/GoTest.sh`](tests/GoTest.sh), [`tests/PHPTest.sh`](tests/PHPTest.sh), [`tests/PythonTest.sh`](tests/PythonTest.sh), [`tests/JsonSchemaTest.sh`](tests/JsonSchemaTest.sh). + +### JSON Schema schema import/export + +- Export a FlatBuffers schema (`.fbs`) to JSON Schema (`.schema.json`). +- Import a JSON Schema (`.schema.json`) as a schema input (as if it were an IDL), map it into FlatBuffers’ schema IR, and run the normal FlatBuffers code generators. + +Implementation: + +- JSON Schema generator: [`src/idl_gen_json_schema.cpp`](src/idl_gen_json_schema.cpp) +- JSON Schema importer/parser: [`src/idl_parser.cpp`](src/idl_parser.cpp) (`Parser::DoParseJsonSchema`) +- CLI wiring + `.schema.json` input detection: [`src/flatc.cpp`](src/flatc.cpp) +- Additional docs: [`docs/source/json_schema.md`](docs/source/json_schema.md), [`docs/source/flatc.md`](docs/source/flatc.md) + +#### Export: FlatBuffers → JSON Schema (`--jsonschema`) + +Generate `*.schema.json` from `*.fbs`: + +```sh +flatc --jsonschema -o out_dir schema.fbs +``` + +This produces `out_dir/schema.schema.json`. + +#### Import: JSON Schema → FlatBuffers IR (`*.schema.json` input) + +Any file ending in `.schema.json` can be used anywhere `flatc` expects a schema file: + +```sh +flatc --cpp -o out_dir schema.schema.json +``` + +Root selection: + +- If the schema root contains a `$ref` to a definition, that definition becomes the FlatBuffers root type. +- Otherwise you can specify/override the root with `--root-type` (see [`src/flatc.cpp`](src/flatc.cpp) and [`docs/source/flatc.md`](docs/source/flatc.md)). + +#### Best-effort mapping for “wild” JSON Schema + OpenAPI + +The importer is intentionally permissive and ignores unknown JSON Schema/OpenAPI keywords while making “sane defaults” to treat the input as an IDL. + +Supported input shapes: + +- Schema definitions under `definitions`, `$defs`, or OpenAPI `components.schemas` (see fixtures under [`tests/jsonschema_import/inputs`](tests/jsonschema_import/inputs)). +- `$ref` resolution for `#/definitions/...`, `#/$defs/...`, and `#/components/schemas/...`. + +Type/shape mapping (when `x-flatbuffers` is not present): + +- `type: "object"` → FlatBuffers table by default; may infer a struct if the definition contains fixed-length arrays and is otherwise “struct-safe” (see [`src/idl_parser.cpp`](src/idl_parser.cpp)). +- `type: "array"` → FlatBuffers vector; if `minItems == maxItems` it may become a fixed-length array (and will fall back to a vector with `minItems`/`maxItems` preserved if a fixed array would be illegal in FlatBuffers). +- `type: "integer"` → a concrete FlatBuffers integer scalar inferred from numeric range (`minimum`/`maximum`) when provided; `format` of `int32`/`int64`/`uint32`/`uint64` overrides inference. +- `type: "number"` → `float` by default; `format` of `float`/`double` overrides. +- `type: "string"` → FlatBuffers `string`. +- String `enum: ["A", "B", ...]` on a field → generates a FlatBuffers enum for that field. +- `anyOf: [{ "$ref": ... }, ...]` on a field → FlatBuffers union. If the input follows the FlatBuffers JSON/JSON-Schema union convention (a value field plus a sibling `_type` field), the importer will link them (see [`src/idl_parser.cpp`](src/idl_parser.cpp)). + +JSON Schema/OpenAPI keyword preservation: + +To keep the generated JSON Schema close to the original, the importer preserves a subset of JSON Schema/OpenAPI keywords (either as FlatBuffers doc flags, or as `jsonschema_*` attributes so they survive through the schema IR), and the JSON Schema generator re-emits them: + +- Definitions + fields: `description` +- Fields: `deprecated` +- Objects: `required`, `additionalProperties` +- Arrays: `minItems`, `maxItems`, `uniqueItems` +- Strings: `format`, `minLength`, `maxLength`, `readOnly` +- Numbers/integers: `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum` + +#### Optional lossless semantics: `x-flatbuffers` (`--jsonschema-xflatbuffers`) + +JSON Schema cannot represent some FlatBuffers semantics (for example: struct vs table, exact scalar widths, union details, field ids, and presence rules). To enable lossless round-trips, the JSON Schema generator can emit an optional vendor extension: + +```sh +flatc --jsonschema --jsonschema-xflatbuffers -o out_dir schema.fbs +``` + +This emits `x-flatbuffers` metadata objects at the schema root, at each definition, and at each field. Because this uses the standard vendor extension mechanism (`x-...`), most JSON Schema tooling ignores it and continues to work normally (for example QuickType and similar code generators). + +At a high level: + +- Root metadata: `root_type`, plus optional `file_identifier` and `file_extension`. +- Definition metadata: enum/union kind + values; struct/table kind + (struct-only) `minalign`/`bytesize`. +- Field metadata: exact FlatBuffers type (including union/enum refs), plus presence and selected field attributes (for example `id`, `deprecated`, `key`). + +The allowed keys/values for the `x-flatbuffers` vendor extension are described by the meta-schema: + +- [`docs/source/schemas/x-flatbuffers.schema.json`](docs/source/schemas/x-flatbuffers.schema.json) + +#### Tests (goldens + round-trip stability) + +This fork uses golden JSON Schemas and round-trip tests to ensure stability: + +- Generation + round-trip for FlatBuffers-emitted JSON Schema (with and without `x-flatbuffers`): [`tests/JsonSchemaTest.sh`](tests/JsonSchemaTest.sh) + - Goldens: [`tests/monster_test.schema.json`](tests/monster_test.schema.json), [`tests/arrays_test.schema.json`](tests/arrays_test.schema.json) +- Import “wild” JSON Schema / OpenAPI fixtures and ensure stable regeneration: [`tests/JsonSchemaImportTest.sh`](tests/JsonSchemaImportTest.sh) + - Inputs: [`tests/jsonschema_import/inputs`](tests/jsonschema_import/inputs) + - Goldens: [`tests/jsonschema_import/goldens`](tests/jsonschema_import/goldens) + +Typical local run: + +```sh +cmake -B build -S . +cmake --build build --target flatc -j +tests/JsonSchemaTest.sh +tests/JsonSchemaImportTest.sh +``` + ![logo](https://flatbuffers.dev/assets/flatbuffers_logo.svg) FlatBuffers ===========