From 38d09ed7b2071d25ccec879c474018823ae73f50 Mon Sep 17 00:00:00 2001 From: Cvikli Date: Tue, 10 Mar 2026 10:51:14 +0100 Subject: [PATCH 1/3] Add FieldError with field name context to parsing errors Previously, when struct parsing failed, errors like MethodError or TypeError were thrown without any indication of which field caused the failure. For example, parsing `{"path": 123}` into a struct with `path::String` would produce: MethodError: Cannot `convert` an object of type Int64 to an object of type String Now the same error produces: FieldError: failed to parse field `path::String` of `Bar`: MethodError: Cannot `convert`... This applies to all failure modes: - Type mismatches: `FieldError: failed to parse field `count::Int64` of `Bar`: MethodError...` - Missing required fields: `FieldError: failed to parse field `count::Int64` of `Bar`: missing required field` - null for non-nullable: `FieldError: failed to parse field `path::String` of `Bar`: MethodError...` - Nested struct failures: `FieldError: failed to parse field `inner::Inner` of `Outer`: ...` The FieldError exception is exported and contains `struct_type`, `field_name`, `field_type`, and `error` fields for programmatic access. --- src/StructUtils.jl | 57 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/src/StructUtils.jl b/src/StructUtils.jl index 6ba1da8..6f07cdf 100644 --- a/src/StructUtils.jl +++ b/src/StructUtils.jl @@ -2,7 +2,25 @@ module StructUtils using Dates, UUIDs -export @noarg, @defaults, @tags, @kwarg, @nonstruct, Selectors +export @noarg, @defaults, @tags, @kwarg, @nonstruct, Selectors, FieldError + +""" + StructUtils.FieldError + +Error thrown when a field fails to parse during struct construction. +Contains the struct type, field name, expected type, and the underlying error. +""" +struct FieldError <: Exception + struct_type::Type + field_name::Symbol + field_type::Type + error::Exception +end + +function Base.showerror(io::IO, e::FieldError) + print(io, "FieldError: failed to parse field `$(e.field_name)::$(e.field_type)` of `$(e.struct_type)`: ") + showerror(io, e.error) +end """ StructUtils.StructStyle @@ -925,6 +943,20 @@ end setval!(vals::T, x, i) where {T} = _setfield!(vals, i, x) +function _make_field(f, ::Type{T}, i, v) where {T} + fn = f.fsyms[i] + FT = fieldtype(T, i) + ftags = fieldtags(f.style, T, fn) + try + val, st = make(f.style, FT, v, ftags) + setval!(f.vals, val, i) + return EarlyReturn(st) + catch e + e isa FieldError && rethrow() + throw(FieldError(T, fn, FT, e)) + end +end + function findfield(::Type{T}, k, v, f) where {T} st = _foreach(T) do i if typeof(k) == Symbol @@ -932,16 +964,11 @@ function findfield(::Type{T}, k, v, f) where {T} ftags = fieldtags(f.style, T, fn) field = get(ftags, :name, fn) if keyeq(k, field) || keyeq(k, fn) - symval, symst = make(f.style, fieldtype(T, i), v, ftags) - setval!(f.vals, symval, i) - return EarlyReturn(symst) + return _make_field(f, T, i, v) end elseif typeof(k) == Int if k == i - ftags = fieldtags(f.style, T, f.fsyms[i]) - intval, intst = make(f.style, fieldtype(T, i), v, ftags) - setval!(f.vals, intval, i) - return EarlyReturn(intst) + return _make_field(f, T, i, v) end else fn = f.fsyms[i] @@ -949,9 +976,7 @@ function findfield(::Type{T}, k, v, f) where {T} ftags = fieldtags(f.style, T, fn) field = get(ftags, :name, fstr) if keyeq(k, field) - strval, strst = make(f.style, fieldtype(T, i), v, ftags) - setval!(f.vals, strval, i) - return EarlyReturn(strst) + return _make_field(f, T, i, v) end end end @@ -989,6 +1014,16 @@ function makestruct(style, ::Type{T}, source) where {T} if T <: NamedTuple return T(_tuple(T, vals, style)), st else + # Check for missing required fields before construction + for i in 1:fieldcount(T) + if !isassigned(vals, i) + try + fielddefault(style, T, fsyms[i])::fieldtype(T, i) + catch e + throw(FieldError(T, fsyms[i], fieldtype(T, i), ArgumentError("missing required field"))) + end + end + end return _construct(T, vals, style, fsyms), st end end From 97f3dc95288e9c0374fe9a649c65c6d6960297a0 Mon Sep 17 00:00:00 2001 From: Cvikli Date: Tue, 10 Mar 2026 10:54:54 +0100 Subject: [PATCH 2/3] Shorten FieldError message format to Bar.field::Type: ... --- src/StructUtils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StructUtils.jl b/src/StructUtils.jl index 6f07cdf..6d131d3 100644 --- a/src/StructUtils.jl +++ b/src/StructUtils.jl @@ -18,7 +18,7 @@ struct FieldError <: Exception end function Base.showerror(io::IO, e::FieldError) - print(io, "FieldError: failed to parse field `$(e.field_name)::$(e.field_type)` of `$(e.struct_type)`: ") + print(io, "FieldError: $(e.struct_type).$(e.field_name)::$(e.field_type): ") showerror(io, e.error) end From f7019ff44cc59abfcc2aa18a1f201f58dcc46c70 Mon Sep 17 00:00:00 2001 From: Cvikli Date: Tue, 10 Mar 2026 10:56:02 +0100 Subject: [PATCH 3/3] Improve missing field error message clarity --- src/StructUtils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StructUtils.jl b/src/StructUtils.jl index 6d131d3..8f8f60e 100644 --- a/src/StructUtils.jl +++ b/src/StructUtils.jl @@ -1020,7 +1020,7 @@ function makestruct(style, ::Type{T}, source) where {T} try fielddefault(style, T, fsyms[i])::fieldtype(T, i) catch e - throw(FieldError(T, fsyms[i], fieldtype(T, i), ArgumentError("missing required field"))) + throw(FieldError(T, fsyms[i], fieldtype(T, i), ArgumentError("required field is missing from input"))) end end end