From 24b89bec905cbacca2fe0f6fe87def3dc78bb84b Mon Sep 17 00:00:00 2001 From: Sina Atalay <79940989+sinaatalay@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:18:17 +0300 Subject: [PATCH] Don't allow extra keys in top schema model --- schema.json | 8 +++++-- src/rendercv/schema/models/rendercv_model.py | 4 ++-- .../variant_pydantic_model_generator.py | 23 +++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/schema.json b/schema.json index 2e8dc645..a0d5cafa 100644 --- a/schema.json +++ b/schema.json @@ -155,7 +155,7 @@ "type": "object" }, "Cv": { - "additionalProperties": true, + "additionalProperties": false, "properties": { "name": { "anyOf": [ @@ -4986,6 +4986,7 @@ "description": "Vertical space between text-based entries. It can be specified with units (cm, in, pt, mm, ex, em). For example, `0.1cm`. The default value is `0.3em`." }, "show_time_spans_in": { + "default": [], "description": "Section titles where time spans (e.g., '2 years 3 months') should be displayed. The default value is `[]`.", "items": { "type": "string" @@ -5017,6 +5018,7 @@ "description": "Vertical space between text-based entries. It can be specified with units (cm, in, pt, mm, ex, em). For example, `0.1cm`. The default value is `0.15cm`." }, "show_time_spans_in": { + "default": [], "description": "Section titles where time spans (e.g., '2 years 3 months') should be displayed. The default value is `[]`.", "items": { "type": "string" @@ -5048,6 +5050,7 @@ "description": "Vertical space between text-based entries. It can be specified with units (cm, in, pt, mm, ex, em). For example, `0.1cm`. The default value is `0.3em`." }, "show_time_spans_in": { + "default": [], "description": "Section titles where time spans (e.g., '2 years 3 months') should be displayed. The default value is `[]`.", "items": { "type": "string" @@ -5079,6 +5082,7 @@ "description": "Vertical space between text-based entries. It can be specified with units (cm, in, pt, mm, ex, em). For example, `0.1cm`. The default value is `0.3em`." }, "show_time_spans_in": { + "default": [], "description": "Section titles where time spans (e.g., '2 years 3 months') should be displayed. The default value is `[]`.", "items": { "type": "string" @@ -5744,7 +5748,7 @@ "type": "string" } }, - "additionalProperties": true, + "additionalProperties": false, "properties": { "cv": { "$ref": "#/$defs/Cv", diff --git a/src/rendercv/schema/models/rendercv_model.py b/src/rendercv/schema/models/rendercv_model.py index 0ac7ef54..49713607 100644 --- a/src/rendercv/schema/models/rendercv_model.py +++ b/src/rendercv/schema/models/rendercv_model.py @@ -2,7 +2,7 @@ import pathlib import pydantic -from .base import BaseModelWithExtraKeys +from .base import BaseModelWithoutExtraKeys from .cv.cv import Cv from .design.classic_theme import ClassicTheme from .design.design import Design @@ -11,7 +11,7 @@ from .settings.settings import Settings from .validation_context import get_input_file_path -class RenderCVModel(BaseModelWithExtraKeys): +class RenderCVModel(BaseModelWithoutExtraKeys): # Technically, `cv` is a required field, but we don't pass it to the JSON Schema # so that the same schema can be used for standalone design, locale, and settings # files. diff --git a/src/rendercv/schema/variant_pydantic_model_generator.py b/src/rendercv/schema/variant_pydantic_model_generator.py index 7580e2d2..eb22ea4e 100644 --- a/src/rendercv/schema/variant_pydantic_model_generator.py +++ b/src/rendercv/schema/variant_pydantic_model_generator.py @@ -9,6 +9,26 @@ from rendercv.exception import RenderCVInternalError type FieldSpec = tuple[type[Any], FieldInfo] +def sanitize_defaults(value: Any) -> Any: + """Recursively convert CommentedMap/CommentedSeq to dict/list. + + Why: + ruamel.yaml returns custom types that behave like dict/list but confuse Pydantic + and JSON schema generation. Stripping metadata ensures clean defaults. + + Args: + value: The value to sanitize (can be nested dict/list structure). + + Returns: + The sanitized value with standard Python types. + """ + if isinstance(value, list): + return [sanitize_defaults(v) for v in value] + if isinstance(value, dict): + return {k: sanitize_defaults(v) for k, v in value.items()} + return value + + def create_variant_pydantic_model[T: pydantic.BaseModel]( variant_name: str, defaults: dict[str, Any], @@ -50,6 +70,9 @@ def create_variant_pydantic_model[T: pydantic.BaseModel]( """ validate_defaults_against_base(defaults, base_class, variant_name) + # Sanitize defaults to remove ruamel.yaml metadata + defaults = sanitize_defaults(defaults) + field_specs: dict[str, Any] = {} base_fields = base_class.model_fields