+11
CHANGELOG.md
+11
CHANGELOG.md
+1
-1
gleam.toml
+1
-1
gleam.toml
+51
-53
src/honk/internal/json_helpers.gleam
+51
-53
src/honk/internal/json_helpers.gleam
···
221
221
222
222
/// Check if dynamic value is null
223
223
pub fn is_null_dynamic(dyn: Dynamic) -> Bool {
224
-
case decode.run(dyn, decode.string) {
225
-
Ok("null") -> True
226
-
_ -> False
227
-
}
224
+
dynamic.classify(dyn) == "Nil"
228
225
}
229
226
230
227
/// Convert JSON object to a dictionary
···
243
240
}
244
241
245
242
/// Convert a dynamic value back to Json
246
-
/// This works by trying different decoders
247
243
pub fn dynamic_to_json(dyn: Dynamic) -> Result(Json, ValidationError) {
248
-
// Try null
249
-
case decode.run(dyn, decode.string) {
250
-
Ok(s) -> {
251
-
case s {
252
-
"null" -> Ok(json.null())
253
-
_ -> Ok(json.string(s))
244
+
case dynamic.classify(dyn) {
245
+
"Nil" -> Ok(json.null())
246
+
"String" -> {
247
+
case decode.run(dyn, decode.string) {
248
+
Ok(s) -> Ok(json.string(s))
249
+
Error(_) -> Error(data_validation("Failed to decode string"))
254
250
}
255
251
}
256
-
Error(_) -> {
257
-
// Try number
252
+
"Int" -> {
258
253
case decode.run(dyn, decode.int) {
259
254
Ok(i) -> Ok(json.int(i))
260
-
Error(_) -> {
261
-
// Try boolean
262
-
case decode.run(dyn, decode.bool) {
263
-
Ok(b) -> Ok(json.bool(b))
264
-
Error(_) -> {
265
-
// Try array
266
-
case decode.run(dyn, decode.list(decode.dynamic)) {
267
-
Ok(arr) -> {
268
-
// Recursively convert array items
269
-
case list.try_map(arr, dynamic_to_json) {
270
-
Ok(json_arr) -> Ok(json.array(json_arr, fn(x) { x }))
271
-
Error(e) -> Error(e)
272
-
}
273
-
}
274
-
Error(_) -> {
275
-
// Try object
276
-
case
277
-
decode.run(dyn, decode.dict(decode.string, decode.dynamic))
278
-
{
279
-
Ok(dict_val) -> {
280
-
// Convert dict to object
281
-
let pairs = dict.to_list(dict_val)
282
-
case
283
-
list.try_map(pairs, fn(pair) {
284
-
let #(key, value_dyn) = pair
285
-
case dynamic_to_json(value_dyn) {
286
-
Ok(value_json) -> Ok(#(key, value_json))
287
-
Error(e) -> Error(e)
288
-
}
289
-
})
290
-
{
291
-
Ok(json_pairs) -> Ok(json.object(json_pairs))
292
-
Error(e) -> Error(e)
293
-
}
294
-
}
295
-
Error(_) ->
296
-
Error(data_validation("Failed to convert dynamic to Json"))
297
-
}
298
-
}
255
+
Error(_) -> Error(data_validation("Failed to decode int"))
256
+
}
257
+
}
258
+
"Float" -> {
259
+
case decode.run(dyn, decode.float) {
260
+
Ok(f) -> Ok(json.float(f))
261
+
Error(_) -> Error(data_validation("Failed to decode float"))
262
+
}
263
+
}
264
+
"Bool" -> {
265
+
case decode.run(dyn, decode.bool) {
266
+
Ok(b) -> Ok(json.bool(b))
267
+
Error(_) -> Error(data_validation("Failed to decode bool"))
268
+
}
269
+
}
270
+
"List" -> {
271
+
case decode.run(dyn, decode.list(decode.dynamic)) {
272
+
Ok(arr) -> {
273
+
case list.try_map(arr, dynamic_to_json) {
274
+
Ok(json_arr) -> Ok(json.array(json_arr, fn(x) { x }))
275
+
Error(e) -> Error(e)
276
+
}
277
+
}
278
+
Error(_) -> Error(data_validation("Failed to decode list"))
279
+
}
280
+
}
281
+
"Dict" -> {
282
+
case decode.run(dyn, decode.dict(decode.string, decode.dynamic)) {
283
+
Ok(dict_val) -> {
284
+
let pairs = dict.to_list(dict_val)
285
+
case
286
+
list.try_map(pairs, fn(pair) {
287
+
let #(key, value_dyn) = pair
288
+
case dynamic_to_json(value_dyn) {
289
+
Ok(value_json) -> Ok(#(key, value_json))
290
+
Error(e) -> Error(e)
299
291
}
300
-
}
292
+
})
293
+
{
294
+
Ok(json_pairs) -> Ok(json.object(json_pairs))
295
+
Error(e) -> Error(e)
301
296
}
302
297
}
298
+
Error(_) -> Error(data_validation("Failed to decode dict"))
303
299
}
304
300
}
301
+
other ->
302
+
Error(data_validation("Unsupported type for JSON conversion: " <> other))
305
303
}
306
304
}
307
305
+92
test/object_validator_test.gleam
+92
test/object_validator_test.gleam
···
99
99
error_message
100
100
|> should.equal("Data validation failed: required field 'title' is missing")
101
101
}
102
+
103
+
// Test nullable field accepts null value
104
+
pub fn nullable_field_accepts_null_test() {
105
+
let schema =
106
+
json.object([
107
+
#("type", json.string("object")),
108
+
#(
109
+
"properties",
110
+
json.object([
111
+
#("name", json.object([#("type", json.string("string"))])),
112
+
#("duration", json.object([#("type", json.string("integer"))])),
113
+
]),
114
+
),
115
+
#("nullable", json.array([json.string("duration")], fn(x) { x })),
116
+
])
117
+
118
+
let data =
119
+
json.object([
120
+
#("name", json.string("test")),
121
+
#("duration", json.null()),
122
+
])
123
+
124
+
let assert Ok(ctx) = context.builder() |> context.build
125
+
let result = field.validate_object_data(data, schema, ctx)
126
+
result |> should.be_ok
127
+
}
128
+
129
+
// Test non-nullable field rejects null value
130
+
pub fn non_nullable_field_rejects_null_test() {
131
+
let schema =
132
+
json.object([
133
+
#("type", json.string("object")),
134
+
#(
135
+
"properties",
136
+
json.object([
137
+
#("name", json.object([#("type", json.string("string"))])),
138
+
#("count", json.object([#("type", json.string("integer"))])),
139
+
]),
140
+
),
141
+
// No nullable array - count cannot be null
142
+
])
143
+
144
+
let data =
145
+
json.object([
146
+
#("name", json.string("test")),
147
+
#("count", json.null()),
148
+
])
149
+
150
+
let assert Ok(ctx) = context.builder() |> context.build
151
+
let result = field.validate_object_data(data, schema, ctx)
152
+
result |> should.be_error
153
+
}
154
+
155
+
// Test nullable field must exist in properties (schema validation)
156
+
pub fn nullable_field_not_in_properties_fails_test() {
157
+
let schema =
158
+
json.object([
159
+
#("type", json.string("object")),
160
+
#(
161
+
"properties",
162
+
json.object([
163
+
#("name", json.object([#("type", json.string("string"))])),
164
+
]),
165
+
),
166
+
// "nonexistent" is not in properties
167
+
#("nullable", json.array([json.string("nonexistent")], fn(x) { x })),
168
+
])
169
+
170
+
let assert Ok(ctx) = context.builder() |> context.build
171
+
let result = field.validate_object_schema(schema, ctx)
172
+
result |> should.be_error
173
+
}
174
+
175
+
// Test valid nullable schema passes validation
176
+
pub fn valid_nullable_schema_test() {
177
+
let schema =
178
+
json.object([
179
+
#("type", json.string("object")),
180
+
#(
181
+
"properties",
182
+
json.object([
183
+
#("name", json.object([#("type", json.string("string"))])),
184
+
#("duration", json.object([#("type", json.string("integer"))])),
185
+
]),
186
+
),
187
+
#("nullable", json.array([json.string("duration")], fn(x) { x })),
188
+
])
189
+
190
+
let assert Ok(ctx) = context.builder() |> context.build
191
+
let result = field.validate_object_schema(schema, ctx)
192
+
result |> should.be_ok
193
+
}