From 2c250a8228e881f7732bc2836cf7b8ac231a755c Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Wed, 14 Jan 2026 01:46:34 -0700 Subject: [PATCH] Fix PR #70 feedback: API cleanup and URL fixes - Change validate() to return nothing on success (matches v1.x behavior) - Extend Base.isvalid instead of exporting custom isvalid - Restore copyright headers in src/schema.jl - Remove main branch from CI.yml (only master exists) - Fix URLs: JuliaServices -> JuliaIO in README.md and docs - Fix migration.md to reflect actual v2.0 API behavior - Update tests for new validate return type Co-Authored-By: Claude Opus 4.5 --- .github/workflows/CI.yml | 2 +- README.md | 15 +++--- docs/make.jl | 2 +- docs/src/migration.md | 103 +++++++++++++++++++-------------------- src/JSONSchema.jl | 2 +- src/compat.jl | 7 +-- src/schema.jl | 73 ++++++++++++++------------- test/runtests.jl | 3 +- test/schema.jl | 13 +++-- 9 files changed, 107 insertions(+), 113 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 85f1e21..8aa14bd 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, master] + branches: [master] tags: ["*"] pull_request: release: diff --git a/README.md b/README.md index b972799..e62b42b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # JSONSchema.jl -[![CI](https://github.com/JuliaServices/JSONSchema.jl/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/JuliaServices/JSONSchema.jl/actions?query=workflow%3ACI) -[![codecov](https://codecov.io/gh/JuliaServices/JSONSchema.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaServices/JSONSchema.jl) -[![Docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliaservices.github.io/JSONSchema.jl/stable) +[![CI](https://github.com/JuliaIO/JSONSchema.jl/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/JuliaIO/JSONSchema.jl/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/JuliaIO/JSONSchema.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaIO/JSONSchema.jl) +[![Docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliaio.github.io/JSONSchema.jl/stable) ## Overview @@ -11,7 +11,7 @@ instances against those schemas. It also supports validating data against hand-written JSON Schema objects. Field-level validation rules are provided via `StructUtils` tags. -> **Upgrading from v1.x?** See the [v2.0 Migration Guide](https://juliaservices.github.io/JSONSchema.jl/stable/migration/) for breaking changes and upgrade instructions. +> **Upgrading from v1.x?** See the [v2.0 Migration Guide](https://juliaio.github.io/JSONSchema.jl/stable/migration/) for breaking changes and upgrade instructions. The test harness is wired to the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) @@ -39,9 +39,8 @@ end schema = JSONSchema.schema(User) user = User(1, "Alice", "alice@example.com") -result = JSONSchema.validate(schema, user) -result.is_valid # true +isvalid(schema, user) # true ``` ### Validate JSON data against a schema object @@ -58,7 +57,7 @@ schema = JSONSchema.Schema(JSON.parse(""" """)) data = JSON.parse("""{"foo": 1}""") -JSONSchema.isvalid(schema, data) # true +isvalid(schema, data) # true ``` ## Features @@ -72,7 +71,7 @@ JSONSchema.isvalid(schema, data) # true ## Documentation -See the [documentation](https://juliaservices.github.io/JSONSchema.jl/stable) for: +See the [documentation](https://juliaio.github.io/JSONSchema.jl/stable) for: - Complete API reference - Validation rules and field tags - Type mapping reference diff --git a/docs/make.jl b/docs/make.jl index 6741ec8..fcb342d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -11,4 +11,4 @@ makedocs( ], ) -deploydocs(repo = "github.com/JuliaServices/JSONSchema.jl.git", push_preview = true) +deploydocs(repo = "github.com/JuliaIO/JSONSchema.jl.git", push_preview = true) diff --git a/docs/src/migration.md b/docs/src/migration.md index 5d3692c..9683809 100644 --- a/docs/src/migration.md +++ b/docs/src/migration.md @@ -10,80 +10,81 @@ JSONSchema.jl v2.0 introduces: - **Schema generation** from Julia types via `schema(T)` - **Type-safe validation** with `Schema{T}` - **StructUtils integration** for field-level validation rules -- **`\$ref` support** for schema deduplication +- **`$ref` support** for schema deduplication Most v1.x code will continue to work with minimal changes thanks to our backwards compatibility layer. ## Breaking Changes -### 1. `validate()` Return Type +### 1. `parent_dir` Keyword Argument Removed -**This is the main breaking change.** The `validate` function now always returns -a `ValidationResult` struct instead of `nothing` on success. +The `Schema` constructor no longer accepts a `parent_dir` keyword argument for +resolving local file `$ref` references. + +**v1.x:** +```julia +schema = Schema(spec; parent_dir="./schemas") +``` + +**v2.0:** Local file reference resolution is not currently supported. External +`$ref` references should be resolved before creating the schema, or use the new +`refs` keyword argument with `schema()` for type-based deduplication. + +### 2. `SingleIssue` Type Replaced by `ValidationResult` + +The `SingleIssue` type from v1.x has been replaced by `ValidationResult`. For +backwards compatibility, `SingleIssue` is aliased to `ValidationResult`, so +existing `isa` checks will continue to work. **v1.x:** ```julia result = validate(schema, data) -if result === nothing - println("Valid!") -else - println(result) # SingleIssue with error details +if result isa SingleIssue + println(result.x) # The invalid value + println(result.path) # JSON path to the error end ``` **v2.0:** ```julia result = validate(schema, data) -if result.is_valid - println("Valid!") -else +if result !== nothing for err in result.errors - println(err) + println(err) # Error message with path end end ``` -**Migration:** Replace `validate(...) === nothing` with `validate(...).is_valid` -or use `isvalid(schema, data)` which returns a boolean. +## API Compatibility -### 2. `JSON.schema`, `JSON.validate`, `JSON.isvalid` No Longer Available +The following v1.x patterns are fully supported in v2.0: -The v1.x package registered convenience methods on the `JSON` module at runtime. -This is no longer supported. +### `validate()` Return Type (Unchanged) -**v1.x:** -```julia -using JSONSchema -JSON.isvalid(schema, data) # Worked via runtime registration -``` +The `validate` function returns `nothing` on success and a `ValidationResult` +on failure, matching v1.x behavior: -**v2.0:** ```julia -using JSONSchema -JSONSchema.isvalid(schema, data) # Use the JSONSchema namespace directly +result = validate(schema, data) +if result === nothing + println("Valid!") +else + for err in result.errors + println(err) + end +end ``` -**Migration:** Replace `JSON.schema`, `JSON.validate`, `JSON.isvalid` with -`JSONSchema.schema`, `JSONSchema.validate`, `JSONSchema.isvalid`. +### `isvalid()` Function -### 3. `parent_dir` Keyword Argument Removed +The `isvalid` function extends `Base.isvalid` and returns a boolean: -The `Schema` constructor no longer accepts a `parent_dir` keyword argument for -resolving local file `\$ref` references. - -**v1.x:** ```julia -schema = Schema(spec; parent_dir="./schemas") -``` - -**v2.0:** Local file reference resolution is not currently supported. External -`\$ref` references should be resolved before creating the schema, or use the new -`refs` keyword argument with `schema()` for type-based deduplication. - -## Compatibility Layer +using JSONSchema -The following v1.x patterns continue to work in v2.0: +isvalid(schema, data) # Returns true or false +``` ### `schema.data` Field Access @@ -119,11 +120,7 @@ isvalid(schema, Dict("bar" => 1)) # Returns false (v1.x behavior) diagnose(data, schema) # Works but emits deprecation warning ``` -### `SingleIssue` Type - -```julia -result isa SingleIssue # Works - SingleIssue is aliased to ValidationResult -``` +Use `validate(schema, data)` instead. ## New Features in v2.0 @@ -151,12 +148,12 @@ Schemas are now parameterized by the type they describe: ```julia schema = JSONSchema.schema(User) # Returns Schema{User} user = User(1, "Alice", "alice@example.com", 30) -JSONSchema.isvalid(schema, user) # Type-safe validation +isvalid(schema, user) # Type-safe validation ``` -### `\$ref` Support for Deduplication +### `$ref` Support for Deduplication -Use `refs=true` to generate schemas with `\$ref` for nested types: +Use `refs=true` to generate schemas with `$ref` for nested types: ```julia @defaults struct Address @@ -170,7 +167,7 @@ end end schema = JSONSchema.schema(Person, refs=true) -# Generates schema with `\$ref` to #/definitions/Address +# Generates schema with `$ref` to #/definitions/Address ``` ### ValidationResult with Error Details @@ -179,7 +176,7 @@ Get detailed validation errors: ```julia result = JSONSchema.validate(schema, invalid_data) -if !result.is_valid +if result !== nothing for error in result.errors println(error) # e.g., "name: string length 0 is less than minimum 1" end @@ -188,13 +185,11 @@ end ## Quick Migration Checklist -- [ ] Replace `validate(...) === nothing` with `validate(...).is_valid` or `isvalid(...)` -- [ ] Replace `JSON.schema/validate/isvalid` with `JSONSchema.schema/validate/isvalid` - [ ] Remove `parent_dir` keyword from `Schema()` calls - [ ] Update error handling to use `ValidationResult.errors` instead of `SingleIssue` fields - [ ] Consider using `schema(T)` for type-based schema generation ## Getting Help -If you encounter issues migrating, please [open an issue](https://github.com/JuliaServices/JSONSchema.jl/issues) +If you encounter issues migrating, please [open an issue](https://github.com/JuliaIO/JSONSchema.jl/issues) with details about your use case. diff --git a/src/JSONSchema.jl b/src/JSONSchema.jl index c2a1b44..348580d 100644 --- a/src/JSONSchema.jl +++ b/src/JSONSchema.jl @@ -6,7 +6,7 @@ import StructUtils import URIs using JSON: JSONWriteStyle, Object -export Schema, SchemaContext, ValidationResult, schema, validate, isvalid +export Schema, SchemaContext, ValidationResult, schema, validate # Backwards compatibility exports (v1.5.0) export diagnose, SingleIssue diff --git a/src/compat.jl b/src/compat.jl index 035cbc7..8d058bc 100644 --- a/src/compat.jl +++ b/src/compat.jl @@ -13,8 +13,9 @@ end # ============= 2. Support inverse argument order ============= # v1.5.0 supported both validate(schema, x) and validate(x, schema) -# NOTE: This is now handled directly in schema.jl via the generic fallback methods -# that check if the second argument is a Schema and swap arguments accordingly. +function validate(instance, schema::Schema; resolver=nothing) + return validate(schema, instance; resolver=resolver) +end # ============= 3. Support boolean schemas ============= # v1.5.0 supported Schema(true) and Schema(false) @@ -75,7 +76,7 @@ function diagnose(x, schema) :diagnose, ) result = validate(schema, x) - if !result.is_valid && !isempty(result.errors) + if result !== nothing && !isempty(result.errors) return join(result.errors, "\n") end return nothing diff --git a/src/schema.jl b/src/schema.jl index fb2f3d3..5f2763d 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -1,3 +1,8 @@ +# Copyright (c) 2018: fredo-dedup and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + # JSON Schema generation and validation from Julia types # Provides a simple, convenient interface for generating JSON Schema v7 specifications @@ -5,7 +10,7 @@ Schema{T} A typed JSON Schema for type `T`. Contains the schema specification and can be used -for validation via `JSON.isvalid`. +for validation via `isvalid` (which overloads `Base.isvalid`). # Fields - `type::Type{T}`: The Julia type this schema describes @@ -13,9 +18,17 @@ for validation via `JSON.isvalid`. # Example ```julia -schema = JSON.schema(User) +using JSONSchema, StructUtils + +@defaults struct User + name::String = "" + email::String = "" + age::Int = 0 +end + +schema = JSONSchema.schema(User) instance = User("alice", "alice@example.com", 25) -is_valid = JSON.isvalid(schema, instance) +isvalid(schema, instance) # returns true ``` """ # Context for tracking type definitions during schema generation with $ref support @@ -759,15 +772,15 @@ struct ValidationResult end """ - validate(schema::Schema{T}, instance::T) -> ValidationResult + validate(schema::Schema{T}, instance::T) -> Union{Nothing, ValidationResult} Validate that `instance` satisfies all constraints defined in `schema`. -Returns a `ValidationResult` containing success status and any error messages. +Returns `nothing` if valid, or a `ValidationResult` containing error messages if invalid. # Example ```julia -result = JSON.validate(schema, instance) -if !result.is_valid +result = validate(schema, instance) +if result !== nothing for err in result.errors println(err) end @@ -778,7 +791,7 @@ function validate(schema::Schema{T}, instance::T; resolver=nothing) where {T} errors = String[] # Pass root schema for \$ref resolution _validate_instance(schema.spec, instance, T, "", errors, false, schema.spec) - return ValidationResult(isempty(errors), errors) + return isempty(errors) ? nothing : ValidationResult(false, errors) end # Also support JSON.Schema (which is an alias for JSONSchema.Schema) @@ -819,12 +832,12 @@ function RefResolver(root; base_uri::AbstractString="", remote_loader=nothing) end """ - isvalid(schema::Schema{T}, instance::T; verbose=false) -> Bool + Base.isvalid(schema::Schema{T}, instance::T; verbose=false) -> Bool Validate that `instance` satisfies all constraints defined in `schema`. -This function checks that the instance meets all validation requirements specified -in the schema's field tags, including: +This function extends `Base.isvalid` and checks that the instance meets all +validation requirements specified in the schema's field tags, including: - String constraints (minLength, maxLength, pattern, format) - Numeric constraints (minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf) - Array constraints (minItems, maxItems, uniqueItems) @@ -841,52 +854,38 @@ in the schema's field tags, including: # Example ```julia -JSON.@defaults struct User +using JSONSchema, StructUtils + +@defaults struct User name::String = "" &(json=(minLength=1, maxLength=100),) age::Int = 0 &(json=(minimum=0, maximum=150),) end -schema = JSON.schema(User) +schema = JSONSchema.schema(User) user1 = User("Alice", 25) user2 = User("", 200) # Invalid: empty name, age too high isvalid(schema, user1) # true isvalid(schema, user2) # false -isvalid(schema, user2, verbose=true) # false, with error messages +isvalid(schema, user2; verbose=true) # false, with error messages ``` """ -function isvalid(schema::Schema{T}, instance::T; verbose::Bool=false) where {T} +function Base.isvalid(schema::Schema{T}, instance::T; verbose::Bool=false) where {T} result = validate(schema, instance) + is_valid = result === nothing - if verbose && !result.is_valid + if verbose && !is_valid for err in result.errors println(" ❌ ", err) end end - return result.is_valid + return is_valid end -# Also support JSON.Schema (which is an alias for JSONSchema.Schema) -# and inverse argument order for v1.5.0 compatibility -function isvalid(schema, instance; verbose::Bool=false) - # Handle inverse argument order (v1.5.0 compat): isvalid(data, schema) - if instance isa Schema - return isvalid(instance, schema; verbose=verbose) - end - - # Handle JSON.Schema (which is aliased to JSONSchema.Schema) - # Since they're the same underlying type, we can just call validate directly - if typeof(schema).name.module === JSON && hasfield(typeof(schema), :type) && hasfield(typeof(schema), :spec) - result = validate(schema, instance) - if verbose && !result.is_valid - for err in result.errors - println(" ❌ ", err) - end - end - return result.is_valid - end - error("Unsupported schema type: $(typeof(schema))") +# Also support inverse argument order for v1.5.0 compatibility +function Base.isvalid(instance, schema::Schema; verbose::Bool=false) + return Base.isvalid(schema, instance; verbose=verbose) end # Internal: Validate an instance against a schema diff --git a/test/runtests.jl b/test/runtests.jl index f06c5d0..373281c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -72,7 +72,8 @@ function run_test_file(draft::String, path::String, data::Vector{UInt8}, failure value = case["data"] @testset "$case_desc" begin result = try - JSONSchema.validate(schema, value; resolver=resolver).is_valid + # validate returns nothing on success, ValidationResult on failure + JSONSchema.validate(schema, value; resolver=resolver) === nothing catch :error end diff --git a/test/schema.jl b/test/schema.jl index a252316..975d1f6 100644 --- a/test/schema.jl +++ b/test/schema.jl @@ -1880,22 +1880,21 @@ end end schema = JSONSchema.schema(ValidateTest) - # Valid + # Valid - validate returns nothing on success instance = ValidateTest(15) res = JSONSchema.validate(schema, instance) - @test res isa JSONSchema.ValidationResult - @test res.is_valid == true - @test isempty(res.errors) - @test JSONSchema.isvalid(schema, instance) == true + @test res === nothing + @test isvalid(schema, instance) == true - # Invalid + # Invalid - validate returns ValidationResult on failure instance_invalid = ValidateTest(5) res_invalid = JSONSchema.validate(schema, instance_invalid) + @test res_invalid isa JSONSchema.ValidationResult @test res_invalid.is_valid == false @test !isempty(res_invalid.errors) @test length(res_invalid.errors) == 1 @test occursin("less than minimum", res_invalid.errors[1]) - @test JSONSchema.isvalid(schema, instance_invalid) == false + @test isvalid(schema, instance_invalid) == false end @testset "Improved Format Validation" begin