Skip to content

Latest commit

 

History

History
463 lines (343 loc) · 14 KB

File metadata and controls

463 lines (343 loc) · 14 KB

schema_traits Guide

Purpose

This document explains how to write func_registry::schema_traits<T> specializations for custom C++ types.

It is written for both humans and LLM-based code generation. If an LLM needs to generate a new schema_traits<T> specialization, it should follow the rules and templates in this file.

What schema_traits<T> Is For

schema_traits<T> describes the JSON schema shape of a C++ type.

It affects schema export only. It does not perform JSON conversion by itself.

Use json_invoke::json_traits<T> when a type also needs JSON input and output conversion. Use func_registry::schema_traits<T> when a type needs a richer exported schema.

In many practical cases, custom domain types need both:

  1. json_invoke::json_traits<T> for runtime conversion.
  2. func_registry::schema_traits<T> for exported JSON schema.

When To Write A Specialization

Write schema_traits<T> when:

  1. T is a custom struct or class exposed in tool parameters or return values.
  2. The default exported schema of object is too vague.
  3. You want nested properties, descriptions, defaults, or examples to appear in exported tool schemas.

Do not write schema_traits<T> for simple scalar types such as:

  1. int
  2. double
  3. bool
  4. std::string

Those already map automatically.

Minimal Template

Use this shape for the smallest useful specialization:

struct Point {
    int x;
    int y;
};

template<>
struct func_registry::schema_traits<Point>
{
    static func_registry::TypeSchema schema()
    {
        using namespace func_registry;

        return objectSchema({
            property("x", integerSchema()),
            property("y", integerSchema()),
        });
    }
};

This means:

  1. The type is exported as a JSON object.
  2. It has properties x and y.
  3. Both properties are required by default.

Common Helper Functions

Use these helpers from include/type_meta/type_schema.hpp.

Scalar Helpers

  • stringSchema()
  • integerSchema()
  • numberSchema()
  • booleanSchema()

Each helper also accepts true to mark the schema as nullable, for example stringSchema(true).

Structural Helpers

  • objectSchema({...})
  • arrayOf(item_schema)
  • dictionaryOf(value_schema)
  • property(name, schema, required)

Notes:

  1. property(name, schema) is required by default.
  2. property(name, schema, false) makes the property optional.
  3. dictionaryOf(value_schema) is for string-key JSON objects such as std::map<std::string, T>.

Metadata Helpers

  • described(schema, text)
  • defaulted(schema, json_literal_text)
  • examples(schema, {json_literal_texts...})

Examples:

described(stringSchema(), "User display name")
defaulted(integerSchema(), "30")
examples(stringSchema(), {"\"Alice\"", "\"Bob\""})

Important:

  1. defaulted and examples accept JSON literals as strings.
  2. Use "30" for a JSON number.
  3. Use "\"Alice\"" for a JSON string value.
  4. Use raw string literals like R"({"name":"Alice","age":30})" for object examples.

Recommended Writing Style

For readability, prefer this pattern:

  1. Put using namespace func_registry; inside schema().
  2. Start with the smallest correct structure.
  3. Add descriptions next.
  4. Add defaults and examples only when they add real value.

Prefer concise field definitions like this:

template<>
struct func_registry::schema_traits<Person>
{
    static func_registry::TypeSchema schema()
    {
        using namespace func_registry;

        return objectSchema({
            property("name", described(stringSchema(), "Display name shown to users.")),
            property("age", described(integerSchema(), "Age in full years.")),
        });
    }
};

If richer metadata is useful, extend it like this:

template<>
struct func_registry::schema_traits<Person>
{
    static func_registry::TypeSchema schema()
    {
        using namespace func_registry;

        return described(
            examples(
                objectSchema({
                    property(
                        "name",
                        examples(
                            defaulted(
                                described(stringSchema(), "Display name shown to users."),
                                "\"Alice\""),
                            {"\"Alice\"", "\"Bob\"", "\"Cara\""})),
                    property(
                        "age",
                        examples(
                            defaulted(
                                described(integerSchema(), "Age in full years."),
                                "30"),
                            {"17", "30", "41"})),
                }),
                {R"({"name":"Alice","age":30})", R"({"name":"Bob","age":17})"}),
            "Person payload used by the JSON invocation demo.");
    }
};

Nested Object Example

If a type contains another custom type, reuse the nested schema through schema_traits<Nested>::schema().

struct Address {
    std::string city;
    std::string country;
};

struct UserProfile {
    std::string name;
    Address address;
};

template<>
struct func_registry::schema_traits<Address>
{
    static func_registry::TypeSchema schema()
    {
        using namespace func_registry;

        return objectSchema({
            property("city", stringSchema()),
            property("country", stringSchema()),
        });
    }
};

template<>
struct func_registry::schema_traits<UserProfile>
{
    static func_registry::TypeSchema schema()
    {
        using namespace func_registry;

        return objectSchema({
            property("name", stringSchema()),
            property("address", schema_traits<Address>::schema()),
        });
    }
};

Container Examples

Array Of Custom Objects

template<>
struct func_registry::schema_traits<Team>
{
    static func_registry::TypeSchema schema()
    {
        using namespace func_registry;

        return objectSchema({
            property("members", arrayOf(schema_traits<Person>::schema())),
        });
    }
};

Dictionary Of Custom Objects

template<>
struct func_registry::schema_traits<Directory>
{
    static func_registry::TypeSchema schema()
    {
        using namespace func_registry;

        return objectSchema({
            property("people_by_id", dictionaryOf(schema_traits<Person>::schema())),
        });
    }
};

Common Type Mapping Table

Use this table when deciding which helper to apply.

C++ shape Typical schema helper Notes
std::string stringSchema() Use for names, ids, labels, free text.
bool booleanSchema() Use for flags and toggles.
int, long, integer enums without enum traits integerSchema() Integer enums become string schemas only when enum_traits<T> provides string mappings.
float, double numberSchema() Use for non-integer numeric values.
std::optional<T> field property(name, <schema for T>, false) and usually nullable schema Optional means not required; nullable means null is allowed. Set both intentionally.
std::vector<T> arrayOf(<schema for T>) Use for ordered lists.
std::array<T, N> arrayOf(<schema for T>) Fixed length is not currently modeled separately.
std::map<std::string, T> dictionaryOf(<schema for T>) Exports as JSON object with additionalProperties.
std::unordered_map<std::string, T> dictionaryOf(<schema for T>) Same export shape as string-key std::map.
Nested custom object Address schema_traits<Address>::schema() Reuse the nested specialization instead of rebuilding inline.
String enum with enum_traits<T> usually reuse nested type schema or rely on automatic enum export Do not duplicate enum values manually unless necessary.

Practical guidance:

  1. Use property("field", stringSchema()) for the simplest scalar field.
  2. Use property("items", arrayOf(schema_traits<Item>::schema())) for object arrays.
  3. Use property("by_id", dictionaryOf(schema_traits<Item>::schema())) for string-key dictionaries.
  4. Use property("nickname", stringSchema(true), false) only when the field is both optional and nullable.

Optional Fields

If a field is not required in JSON, mark it as optional with the third argument to property.

property("nickname", stringSchema(true), false)

This means:

  1. The field is nullable.
  2. The field is not required.

Use both settings intentionally. Do not mark a field nullable unless null is a valid JSON input.

Relationship To Enum Support

String-based enums are usually handled through func_registry::enum_traits<T>.

If a field type already resolves to a string enum schema automatically, do not manually duplicate the enum values inside schema_traits<T> unless you have a strong reason.

Prefer reusing the nested type schema when possible.

Rules For LLM Code Generation

If an LLM is asked to generate schema_traits<T>, it should follow these rules:

  1. Generate the specialization in the same support header that already holds nearby type support code.
  2. Use the minimal template first.
  3. Use objectSchema for structs and classes that serialize as JSON objects.
  4. Add one property(...) per JSON field.
  5. Use arrayOf(...) for vectors and arrays.
  6. Use dictionaryOf(...) only for string-key maps.
  7. Reuse schema_traits<Nested>::schema() for nested custom types.
  8. Add descriptions only when they are concrete and useful.
  9. Add defaults and examples only when they are valid JSON literals.
  10. Keep field names aligned with json_traits<T> or runtime JSON conversion behavior.

Prompt Template Reference

If you want an LLM to generate code directly, this section provides reusable prompt templates.

Recommended flow:

  1. Attach this file as the rule document.
  2. Copy one of the prompt templates below.
  3. Attach the target type definition and any existing json_traits<T> code.
  4. Ask for only the schema_traits<T> specialization.

Full Prompt Template

Use this when the LLM does not already have the repository context loaded.

You are generating a func_registry::schema_traits<T> specialization for this repository.

Follow the rules in SCHEMA_TRAITS.md strictly.

Requirements:
1. Reuse the actual JSON field names used by runtime conversion.
2. Keep the specialization in the same support header as nearby type support code when possible.
3. Prefer the minimal correct schema first.
4. Use the helper API from include/type_meta/type_schema.hpp.
5. Reuse schema_traits<Nested>::schema() for nested custom types.
6. Use arrayOf(...) for vectors/arrays.
7. Use dictionaryOf(...) only for string-key maps.
8. Mark optional fields with property(name, schema, false).
9. Only add descriptions, defaults, and examples when they are concrete and valid.
10. Do not invent fields that are not supported by the actual runtime JSON shape.

Output requirements:
1. Return only the C++ code for the specialization.
2. Do not explain the code.
3. Do not change unrelated code.
4. Keep the style consistent with the repository.

Target type:
<paste type definition here>

Existing JSON conversion code if available:
<paste json_traits<T> or equivalent conversion code here>

Short Prompt Template

Use this when the LLM already has the repository context loaded.

Write func_registry::schema_traits<T> for the type below.

Follow SCHEMA_TRAITS.md.
Match the runtime JSON shape exactly.
Use the shorter helper API from type_schema.hpp.
Prefer the smallest correct schema.
Reuse nested schema_traits when needed.
Return only the specialization code.

Target type:
<paste type definition here>

Existing JSON conversion code if available:
<paste json_traits<T> or equivalent conversion code here>

Review Prompt Template

Use this when you already have a generated specialization and want the LLM to validate it.

Review this func_registry::schema_traits<T> specialization against SCHEMA_TRAITS.md.

Check:
1. Field names match runtime JSON conversion.
2. Required vs optional is correct.
3. Nullability is correct.
4. Nested custom types reuse nested schema_traits.
5. Defaults and examples are valid JSON literals.
6. No extra fields were invented.
7. The code uses the repository helper API consistently.

If there are problems, list them precisely.
If it is correct, say it is correct.

Type definition:
<paste type definition here>

Runtime JSON conversion code:
<paste json_traits<T> or equivalent conversion code here>

schema_traits code:
<paste specialization here>

Suggested Attachment Set For LLM Workflows

When using an LLM tool or agent, attach these materials together:

  1. This file.
  2. The target support header.
  3. The target type definition header.
  4. Existing json_traits<T> code if present.

That bundle is usually enough for the model to generate a correct first draft.

Rules For Humans Reviewing LLM Output

When reviewing generated code, check these items:

  1. Field names exactly match the JSON payload shape.
  2. Required vs optional is correct.
  3. Nullability is correct.
  4. Nested custom types reuse their own schema_traits.
  5. defaulted(...) values are valid JSON literals.
  6. examples(...) values are valid JSON literals.
  7. The schema does not invent fields that runtime conversion does not support.

Preferred Placement In This Repository

In this repository, keep schema_traits<T> next to related type support code.

Examples:

  1. tests/json_invoke_tests.cpp co-locates json_traits<Person> and schema_traits<Person> for snapshot coverage.
  2. The same file co-locates enum mappings via enum_traits<Priority>.

That organization is easier for both humans and LLMs to follow than scattering traits across many unrelated files.

Quick Checklist

Before finishing a schema_traits<T> specialization, verify:

  1. The type is actually exposed in tool parameters or return values.
  2. The field list matches runtime JSON conversion.
  3. Nested objects and containers are represented structurally.
  4. Optional and nullable semantics are intentional.
  5. Descriptions, defaults, and examples are concrete and valid.
  6. The result is readable enough that another person or LLM can extend it later.