Skip to content

Conversation

@loumalouomega
Copy link
Member

📝 Description

1_ggoPUVhdqpyxZXzBJxnjfw

This PR introduces a powerful new feature to the Kratos::Parameters class: the ability to validate its contents against a formal JSON Schema.

Library considered

This is based on the pboettch/json-schema-validator library ("pseudo-official" to nlohmann), and the validated version is JSON Schema Draft 7. The version integrated is a header only, from here pboettch/json-schema-validator#360, to avoid potential issues from the source/header version. For the nlohmann/json we are using the header only too to avoid compiling issues.

Version considered

You can typically identify the version a schema is written for by looking at the $schema keyword at the root of the schema document. For Draft 7, this is:

{
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Why this is important:

  • Draft 7 is a very stable and widely adopted version of the standard, so you will find plenty of documentation and examples for it.
  • It is the version immediately preceding the newer, date-based versions (like 2019-09 and 2020-12), which introduced more significant changes.
  • For consistency and to ensure the validator applies the correct set of rules, it is always best practice to include the $schema keyword in all your schema files.

Motivation

Currently, Parameters validation relies on methods like ValidateAndAssignDefaults, which compares an input against a default Parameters object. While useful for setting defaults and checking for extraneous keys, this approach has limitations:

  • It's a Kratos-specific implementation, not a recognized standard.
  • It cannot enforce complex validation rules such as numerical ranges (minimum, maximum), string patterns (regex), enumerated values (enum), or conditional requirements.

By integrating the industry-standard JSON Schema, we provide a much more expressive and robust mechanism for ensuring that user-provided parameters are correct. This leads to:

  • Improved Robustness: Catches configuration errors early with clear, specific messages.
  • Enhanced Clarity: Schemas act as machine-readable documentation for the expected input format of any component.
  • Standardization: Aligns Kratos with common practices in modern software development.

Implementation Details

  1. New Validate() Method:

    • A new public methods has been added to the Kratos::Parameters class:
      • void Validate(const Parameters& rSchema) const;
    • This methods leverage the pboettch/json-schema-validator library to perform a full, recursive validation of the Parameters object against the provided schema.
    • Exposed to Python as well.
  2. Error Handling:

    • If validation fails, the method throws a KRATOS_ERROR. The error message is detailed and user-friendly, including:
      • The specific validation error from the library (e.g., "Wrong type for 'max_iteration', expected integer, got string").
      • The full Parameters object that failed validation.
      • The schema that was used for the check.
    • A specific KRATOS_ERROR is also thrown if the provided schema itself is invalid, aiding in debugging.
  3. Dependencies:

    • This feature relies on the pboettch/json-schema-validator single-header library, which has been included in the project as a header only library (MIT license).

Testing

To ensure the reliability of this new feature, comprehensive tests have been added at both the C++ and Python levels.

  1. C++ Tests (test_kratos_parameters.cpp):

    • A new test suite, KratosParametersJSONSchemaValidation, has been added.
    • It covers the following scenarios:
      • Successful validation of a conforming Parameters object.
      • Failure due to incorrect data types.
      • Failure due to a missing required property.
      • Failure due to a numerical constraint violation (minimum, exclusiveMinimum).
      • Failure within a nested object to confirm recursive validation.
      • Correct error handling when the provided schema itself is malformed.
    • Minor enhancements were also added to existing tests for RemoveValue to improve coverage.
  2. Python Tests (test_kratos_parameters.py):

    • A new test class, TestParametersJSONSchemaValidation, has been added to mirror the C++ tests.
    • This ensures that the Python bindings for the Validate method are working correctly and that exceptions are properly propagated to the Python layer.

Examples

C++ Example

This code snippet shows how you would integrate the validation within a C++ function.

#include "includes/kratos_parameters.h"
#include "input_output/logger.h"

void ExampleSolverSetup()
{
    // 1. Define the schema as a Kratos::Parameters object.
    // This schema defines the expected structure, types, and constraints for solver settings.
    Kratos::Parameters solver_schema(R"({
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "SolverSettings",
        "type": "object",
        "properties": {
            "solver_type": {
                "type": "string",
                "enum": ["AMGCL", "GMRES"]
            },
            "max_iteration": {
                "type": "integer",
                "minimum": 1
            },
            "preconditioner": {
                "type": "object",
                "properties": {
                    "preconditioner_type": { "type": "string" }
                },
                "required": ["preconditioner_type"]
            }
        },
        "required": ["solver_type", "max_iteration"]
    })");

    // 2. Create a user-provided settings object that conforms to the schema.
    Kratos::Parameters valid_settings(R"({
        "solver_type": "AMGCL",
        "max_iteration": 500
    })");

    // 3. Create another settings object that violates the schema constraints.
    Kratos::Parameters invalid_settings(R"({
        "solver_type": "GMRES",
        "max_iteration": 0
    })");

    // 4. Perform validation

    // This validation will pass silently.
    valid_settings.Validate(solver_schema);
    KRATOS_INFO("SchemaValidationExample") << "The 'valid_settings' object was successfully validated." << std::endl;

    // This validation will throw a KRATOS_ERROR because "max_iteration" is 0,
    // violating the "minimum: 1" constraint in the schema.
    invalid_settings.Validate(solver_schema);
}

Python Example

This script shows the equivalent operation using the Python bindings. The workflow is identical, using a standard try...except block to handle the RuntimeError that is thrown from C++.

from KratosMultiphysics import Parameters

def example_solver_setup_python():
    """Demonstrates JSON Schema validation using Kratos Parameters in Python."""

    # 1. Define the schema as a Parameters object.
    # The schema string is identical to the C++ example.
    solver_schema = Parameters("""{
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "SolverSettings",
        "type": "object",
        "properties": {
            "solver_type": {
                "type": "string",
                "enum": ["AMGCL", "GMRES"]
            },
            "max_iteration": {
                "type": "integer",
                "minimum": 1
            },
            "preconditioner": {
                "type": "object",
                "properties": {
                    "preconditioner_type": { "type": "string" }
                },
                "required": ["preconditioner_type"]
            }
        },
        "required": ["solver_type", "max_iteration"]
    }""")

    # 2. Create a user-provided settings object that conforms to the schema.
    valid_settings = Parameters("""{
        "solver_type": "AMGCL",
        "max_iteration": 500
    }""")

    # 3. Create another settings object that violates the schema constraints.
    invalid_settings = Parameters("""{
        "solver_type": "GMRES",
        "max_iteration": 0
    }""")

    # 4. Perform validation using a standard Python try/except block.
    try:
        # This validation will pass silently.
        valid_settings.Validate(solver_schema)
        print("✅ The 'valid_settings' object was successfully validated.")

        # This validation will throw a RuntimeError because "max_iteration" is 0,
        # violating the "minimum: 1" constraint in the schema.
        invalid_settings.Validate(solver_schema)

    except RuntimeError as e:
        # The C++ KRATOS_ERROR is caught as a Python RuntimeError.
        print("\\n❌ Caught expected validation error:")
        print(e)

if __name__ == '__main__':
    example_solver_setup_python()

🆕 Changelog

Copy link
Contributor

@matekelemen matekelemen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. There's already a PR for this #13131. It has a reference implementation for integrating it into Kratos.
  2. I thought schemas were waiting to be discussed by the @KratosMultiphysics/technical-committee. Specifically, I thought we first needed to agree on where to store schemas.
  3. Do I really need to start using AI to summarize the extremely verbose AI-generated PR description?

Comment on lines 1509 to 1522
try {
// Validate the current object's underlying nlohmann::json (mpValue).
validator.validate(*mpValue);
} catch (const std::exception& e) {
// 4. On validation failure, catch the standard exception and re-throw a detailed KRATOS_ERROR.
std::stringstream msg;
msg << "JSON schema validation failed.\n";
msg << "Validation Error: " << e.what() << "\n\n";
msg << "Parameters object being validated:\n" << this->PrettyPrintJsonString() << "\n\n";
msg << "Schema used for validation:\n" << rSchema.PrettyPrintJsonString() << "\n";
KRATOS_ERROR << msg.str() << std::endl;
}

KRATOS_CATCH("")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of a try-catch block if you already have it enclosed in KRATOS_TRY - KRATOS_CATCH? This reeks of AI.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, is copy paste

Comment on lines +21 to +34
# New in version 2

Although significant changes have been done for the 2nd version
(a complete rewrite) the API is compatible with the 1.0.0 release. Except for
the namespace which is now `nlohmann::json_schema`.

Version **2** supports JSON schema draft 7, whereas 1 was supporting draft 4
only. Please update your schemas.

The primary change in 2 is the way a schema is used. While in version 1 the schema was
kept as a JSON-document and used again and again during validation, in version 2 the schema
is parsed into compiled C++ objects which are then used during validation. There are surely
still optimizations to be done, but validation speed has improved by factor 100
or more.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yet another compressed external lib that doesn't say anything about its actual version.

@loumalouomega
Copy link
Member Author

loumalouomega commented Sep 12, 2025

  1. There's already a PR for this [Core] Add JSON Schemas #13131. It has a reference implementation for integrating it into Kratos.

You said it was not compiling, I said it could be fixed using a header only solution. Additionally; i dind't know about this PR.

2. I thought schemas were waiting to be discussed by the @KratosMultiphysics/technical-committee. Specifically, I thought we first needed to agree on where to store schemas.

This just adds the validation. It is a minimal interface.

3. Do I really need to start using AI to summarize the extremely verbose AI-generated PR description?

You can do as you like.

@matekelemen
Copy link
Contributor

You said it was not compiling, I said it could be fixed using a header only solution.

The problem is that we currently apply the same compiler flags to external libs as our own sources, including warnings and treating warnings as errors. To solve this, we'd need to revamp our CMake lists to use target-based properties rather than global ones, which again would need to be discussed in the TC.

@loumalouomega
Copy link
Member Author

You said it was not compiling, I said it could be fixed using a header only solution.

The problem is that we currently apply the same compiler flags to external libs as our own sources, including warnings and treating warnings as errors. To solve this, we'd need to revamp our CMake lists to use target-based properties rather than global ones, which again would need to be discussed in the TC.

Header only is simpler... and we already do header only with json.

Copy link
Contributor

@matekelemen matekelemen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm removing my block because I'll be on holiday until mid-October, but please wait for a review from the @KratosMultiphysics/technical-committee. I disabled automerge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants