1package jsonrepair
2
3import (
4 "encoding/json"
5 "reflect"
6 "strings"
7 "testing"
8)
9
10func TestRepairJSON(t *testing.T) {
11 cases := []struct {
12 name string
13 input string
14 opts []Option
15 want string
16 }{
17 {
18 name: "valid_object",
19 input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
20 want: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
21 },
22 {
23 name: "array_spacing",
24 input: "{\"employees\":[\"John\", \"Anna\", \"Peter\"]} ",
25 want: "{\"employees\": [\"John\", \"Anna\", \"Peter\"]}",
26 },
27 {
28 name: "colon_in_string",
29 input: "{\"key\": \"value:value\"}",
30 want: "{\"key\": \"value:value\"}",
31 },
32 {
33 name: "trailing_comma_in_string",
34 input: "{\"text\": \"The quick brown fox,\"}",
35 want: "{\"text\": \"The quick brown fox,\"}",
36 },
37 {
38 name: "apostrophe_in_string",
39 input: "{\"text\": \"The quick brown fox won't jump\"}",
40 want: "{\"text\": \"The quick brown fox won't jump\"}",
41 },
42 {
43 name: "missing_brace",
44 input: "{\"key\": \"\"",
45 want: "{\"key\": \"\"}",
46 },
47 {
48 name: "nested_object",
49 input: "{\"key1\": {\"key2\": [1, 2, 3]}}",
50 want: "{\"key1\": {\"key2\": [1, 2, 3]}}",
51 },
52 {
53 name: "large_integer",
54 input: "{\"key\": 12345678901234567890}",
55 want: "{\"key\": 12345678901234567890}",
56 },
57 {
58 name: "unicode_escape",
59 input: "{\"key\": \"value☺\"}",
60 want: "{\"key\": \"value\\u263a\"}",
61 },
62 {
63 name: "escaped_newline",
64 input: "{\"key\": \"value\\nvalue\"}",
65 want: "{\"key\": \"value\\nvalue\"}",
66 },
67 }
68
69 for _, tc := range cases {
70 t.Run(tc.name, func(t *testing.T) {
71 got, err := RepairJSON(tc.input, tc.opts...)
72 if err != nil {
73 t.Fatalf("unexpected error: %v", err)
74 }
75 if got != tc.want {
76 t.Fatalf("got %q want %q", got, tc.want)
77 }
78 })
79 }
80}
81
82func TestRepairJSONMultipleTopLevel(t *testing.T) {
83 cases := []struct {
84 name string
85 input string
86 want string
87 }{
88 {
89 name: "array_then_object",
90 input: "[]{}",
91 want: "[]",
92 },
93 {
94 name: "array_then_object_with_value",
95 input: "[]{\"key\":\"value\"}",
96 want: "{\"key\": \"value\"}",
97 },
98 {
99 name: "object_then_array",
100 input: "{\"key\":\"value\"}[1,2,3,True]",
101 want: "[{\"key\": \"value\"}, [1, 2, 3, true]]",
102 },
103 {
104 name: "embedded_code_blocks",
105 input: "lorem ```json {\"key\":\"value\"} ``` ipsum ```json [1,2,3,True] ``` 42",
106 want: "[{\"key\": \"value\"}, [1, 2, 3, true]]",
107 },
108 {
109 name: "array_followed_by_array",
110 input: "[{\"key\":\"value\"}][{\"key\":\"value_after\"}]",
111 want: "[{\"key\": \"value_after\"}]",
112 },
113 }
114
115 for _, tc := range cases {
116 t.Run(tc.name, func(t *testing.T) {
117 got, err := RepairJSON(tc.input)
118 if err != nil {
119 t.Fatalf("unexpected error: %v", err)
120 }
121 if got != tc.want {
122 t.Fatalf("got %q want %q", got, tc.want)
123 }
124 })
125 }
126}
127
128func TestRepairJSONEnsureASCII(t *testing.T) {
129 got, err := RepairJSON("{'test_中国人_ascii':'统一码'}", WithEnsureASCII(false))
130 if err != nil {
131 t.Fatalf("unexpected error: %v", err)
132 }
133 want := "{\"test_中国人_ascii\": \"统一码\"}"
134 if got != want {
135 t.Fatalf("got %q want %q", got, want)
136 }
137}
138
139func TestRepairJSONStreamStable(t *testing.T) {
140 cases := []struct {
141 name string
142 input string
143 opts []Option
144 want string
145 }{
146 {
147 name: "default_trailing_backslash",
148 input: "{\"key\": \"val\\",
149 want: "{\"key\": \"val\\\\\"}",
150 },
151 {
152 name: "default_trailing_newline",
153 input: "{\"key\": \"val\\n",
154 want: "{\"key\": \"val\"}",
155 },
156 {
157 name: "default_split_object",
158 input: "{\"key\": \"val\\n123,`key2:value2",
159 want: "{\"key\": \"val\\n123\", \"key2\": \"value2\"}",
160 },
161 {
162 name: "stable_trailing_backslash",
163 input: "{\"key\": \"val\\",
164 opts: []Option{WithStreamStable()},
165 want: "{\"key\": \"val\"}",
166 },
167 {
168 name: "stable_trailing_newline",
169 input: "{\"key\": \"val\\n",
170 opts: []Option{WithStreamStable()},
171 want: "{\"key\": \"val\\n\"}",
172 },
173 {
174 name: "stable_split_object",
175 input: "{\"key\": \"val\\n123,`key2:value2",
176 opts: []Option{WithStreamStable()},
177 want: "{\"key\": \"val\\n123,`key2:value2\"}",
178 },
179 {
180 name: "stable_complete_stream",
181 input: "{\"key\": \"val\\n123,`key2:value2`\"}",
182 opts: []Option{WithStreamStable()},
183 want: "{\"key\": \"val\\n123,`key2:value2`\"}",
184 },
185 }
186
187 for _, tc := range cases {
188 t.Run(tc.name, func(t *testing.T) {
189 got, err := RepairJSON(tc.input, tc.opts...)
190 if err != nil {
191 t.Fatalf("unexpected error: %v", err)
192 }
193 if got != tc.want {
194 t.Fatalf("got %q want %q", got, tc.want)
195 }
196 })
197 }
198}
199
200func TestLoads(t *testing.T) {
201 cases := []struct {
202 name string
203 input string
204 want any
205 }{
206 {
207 name: "empty_array",
208 input: "[]",
209 want: []any{},
210 },
211 {
212 name: "empty_object",
213 input: "{}",
214 want: map[string]any{},
215 },
216 {
217 name: "bools_nulls",
218 input: "{\"key\": true, \"key2\": false, \"key3\": null}",
219 want: map[string]any{
220 "key": true,
221 "key2": false,
222 "key3": nil,
223 },
224 },
225 {
226 name: "simple_object",
227 input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
228 want: map[string]any{
229 "name": "John",
230 "age": json.Number("30"),
231 "city": "New York",
232 },
233 },
234 {
235 name: "array_numbers",
236 input: "[1, 2, 3, 4]",
237 want: []any{
238 json.Number("1"),
239 json.Number("2"),
240 json.Number("3"),
241 json.Number("4"),
242 },
243 },
244 {
245 name: "string_array",
246 input: "{\"employees\":[\"John\", \"Anna\", \"Peter\"]} ",
247 want: map[string]any{
248 "employees": []any{"John", "Anna", "Peter"},
249 },
250 },
251 {
252 name: "string_quotes_repaired",
253 input: "[{\"foo\": \"foo bar \"foobar\" foo bar baz.\", \"tag\": \"#foo-bar-foobar\"}]",
254 want: []any{
255 map[string]any{
256 "foo": "foo bar \"foobar\" foo bar baz.",
257 "tag": "#foo-bar-foobar",
258 },
259 },
260 },
261 }
262
263 for _, tc := range cases {
264 t.Run(tc.name, func(t *testing.T) {
265 got, err := Loads(tc.input)
266 if err != nil {
267 t.Fatalf("unexpected error: %v", err)
268 }
269 if !reflect.DeepEqual(got, tc.want) {
270 t.Fatalf("got %#v want %#v", got, tc.want)
271 }
272 })
273 }
274}
275
276func TestRepairJSONSkipJSONLoads(t *testing.T) {
277 cases := []struct {
278 name string
279 input string
280 opts []Option
281 want string
282 }{
283 {
284 name: "valid_json",
285 input: "{\"key\": true, \"key2\": false, \"key3\": null}",
286 opts: []Option{WithSkipJSONLoads()},
287 want: "{\"key\": true, \"key2\": false, \"key3\": null}",
288 },
289 {
290 name: "missing_value",
291 input: "{\"key\": true, \"key2\": false, \"key3\": }",
292 opts: []Option{WithSkipJSONLoads()},
293 want: "{\"key\": true, \"key2\": false, \"key3\": \"\"}",
294 },
295 }
296
297 for _, tc := range cases {
298 t.Run(tc.name, func(t *testing.T) {
299 got, err := RepairJSON(tc.input, tc.opts...)
300 if err != nil {
301 t.Fatalf("unexpected error: %v", err)
302 }
303 if got != tc.want {
304 t.Fatalf("got %q want %q", got, tc.want)
305 }
306 })
307 }
308
309 got, err := Loads("{\"key\": true, \"key2\": false, \"key3\": }", WithSkipJSONLoads())
310 if err != nil {
311 t.Fatalf("unexpected error: %v", err)
312 }
313 want := map[string]any{
314 "key": true,
315 "key2": false,
316 "key3": "",
317 }
318 if !reflect.DeepEqual(got, want) {
319 t.Fatalf("got %#v want %#v", got, want)
320 }
321}
322
323func TestRepairJSONWithLog(t *testing.T) {
324 cases := []struct {
325 name string
326 input string
327 wantValue any
328 wantLog []LogEntry
329 }{
330 {
331 name: "valid_json",
332 input: "{}",
333 wantValue: map[string]any{},
334 wantLog: []LogEntry{},
335 },
336 {
337 name: "missing_quote",
338 input: "{\"key\": \"value}",
339 wantValue: map[string]any{
340 "key": "value",
341 },
342 wantLog: []LogEntry{
343 {
344 Context: "y\": \"value}",
345 Text: "While parsing a string missing the left delimiter in object value context, we found a , or } and we couldn't determine that a right delimiter was present. Stopping here",
346 },
347 {
348 Context: "y\": \"value}",
349 Text: "While parsing a string, we missed the closing quote, ignoring",
350 },
351 },
352 },
353 }
354
355 for _, tc := range cases {
356 t.Run(tc.name, func(t *testing.T) {
357 gotValue, gotLog, err := RepairJSONWithLog(tc.input)
358 if err != nil {
359 t.Fatalf("unexpected error: %v", err)
360 }
361 if !reflect.DeepEqual(gotValue, tc.wantValue) {
362 t.Fatalf("got %#v want %#v", gotValue, tc.wantValue)
363 }
364 if !reflect.DeepEqual(gotLog, tc.wantLog) {
365 t.Fatalf("got %#v want %#v", gotLog, tc.wantLog)
366 }
367 })
368 }
369}
370
371func TestRepairJSONStrict(t *testing.T) {
372 cases := []struct {
373 name string
374 input string
375 opts []Option
376 wantErr string
377 }{
378 {
379 name: "multiple_top_level",
380 input: "{\"key\":\"value\"}[\"value\"]",
381 opts: []Option{WithStrict()},
382 wantErr: "multiple top-level JSON elements",
383 },
384 {
385 name: "duplicate_keys_in_array",
386 input: "[{\"key\": \"first\", \"key\": \"second\"}]",
387 opts: []Option{WithStrict(), WithSkipJSONLoads()},
388 wantErr: "duplicate key found",
389 },
390 {
391 name: "empty_key",
392 input: "{\"\" : \"value\"}",
393 opts: []Option{WithStrict(), WithSkipJSONLoads()},
394 wantErr: "empty key found",
395 },
396 {
397 name: "missing_colon",
398 input: "{\"missing\" \"colon\"}",
399 opts: []Option{WithStrict()},
400 wantErr: "missing ':' after key",
401 },
402 {
403 name: "empty_value",
404 input: "{\"key\": , \"key2\": \"value2\"}",
405 opts: []Option{WithStrict(), WithSkipJSONLoads()},
406 wantErr: "parsed value is empty",
407 },
408 {
409 name: "empty_object_with_extra",
410 input: "{\"dangling\"}",
411 opts: []Option{WithStrict()},
412 wantErr: "parsed object is empty",
413 },
414 {
415 name: "immediate_doubled_quotes",
416 input: "{\"key\": \"\"\"\"}",
417 opts: []Option{WithStrict()},
418 wantErr: "doubled quotes followed by another quote",
419 },
420 {
421 name: "doubled_quotes_followed_by_string",
422 input: "{\"key\": \"\" \"value\"}",
423 opts: []Option{WithStrict()},
424 wantErr: "doubled quotes followed by another quote while parsing a string",
425 },
426 }
427
428 for _, tc := range cases {
429 t.Run(tc.name, func(t *testing.T) {
430 _, err := RepairJSON(tc.input, tc.opts...)
431 if err == nil {
432 t.Fatalf("expected error")
433 }
434 if !strings.Contains(err.Error(), tc.wantErr) {
435 t.Fatalf("got %q want %q", err.Error(), tc.wantErr)
436 }
437 })
438 }
439}
440
441func TestParseArrayObjects(t *testing.T) {
442 cases := []struct {
443 name string
444 input string
445 want any
446 }{
447 {
448 name: "empty_array",
449 input: "[]",
450 want: []any{},
451 },
452 {
453 name: "numbers_array",
454 input: "[1, 2, 3, 4]",
455 want: []any{
456 json.Number("1"),
457 json.Number("2"),
458 json.Number("3"),
459 json.Number("4"),
460 },
461 },
462 {
463 name: "unfinished_array",
464 input: "[",
465 want: []any{},
466 },
467 }
468
469 for _, tc := range cases {
470 t.Run(tc.name, func(t *testing.T) {
471 got, err := Loads(tc.input)
472 if err != nil {
473 t.Fatalf("unexpected error: %v", err)
474 }
475 if !reflect.DeepEqual(got, tc.want) {
476 t.Fatalf("got %#v want %#v", got, tc.want)
477 }
478 })
479 }
480}
481
482func TestParseArrayEdgeCases(t *testing.T) {
483 cases := []struct {
484 name string
485 input string
486 want string
487 }{
488 {
489 name: "nested_newlines",
490 input: "[[1\n\n]",
491 want: "[[1]]",
492 },
493 {
494 name: "array_with_object_end",
495 input: "[{]",
496 want: "[]",
497 },
498 {
499 name: "just_open_bracket",
500 input: "[",
501 want: "[]",
502 },
503 {
504 name: "dangling_quote",
505 input: "[\"",
506 want: "[]",
507 },
508 {
509 name: "just_close_bracket",
510 input: "]",
511 want: "",
512 },
513 {
514 name: "trailing_comma",
515 input: "[1, 2, 3,",
516 want: "[1, 2, 3]",
517 },
518 {
519 name: "ellipsis_end",
520 input: "[1, 2, 3, ...]",
521 want: "[1, 2, 3]",
522 },
523 {
524 name: "ellipsis_middle",
525 input: "[1, 2, ... , 3]",
526 want: "[1, 2, 3]",
527 },
528 {
529 name: "ellipsis_string",
530 input: "[1, 2, '...', 3]",
531 want: "[1, 2, \"...\", 3]",
532 },
533 {
534 name: "ellipsis_bools",
535 input: "[true, false, null, ...]",
536 want: "[true, false, null]",
537 },
538 {
539 name: "missing_commas",
540 input: "[\"a\" \"b\" \"c\" 1",
541 want: "[\"a\", \"b\", \"c\", 1]",
542 },
543 {
544 name: "object_array_missing_end",
545 input: "{\"employees\":[\"John\", \"Anna\",",
546 want: "{\"employees\": [\"John\", \"Anna\"]}",
547 },
548 {
549 name: "object_array_missing_quote",
550 input: "{\"employees\":[\"John\", \"Anna\", \"Peter",
551 want: "{\"employees\": [\"John\", \"Anna\", \"Peter\"]}",
552 },
553 {
554 name: "nested_object_array",
555 input: "{\"key1\": {\"key2\": [1, 2, 3",
556 want: "{\"key1\": {\"key2\": [1, 2, 3]}}",
557 },
558 {
559 name: "missing_array_quote",
560 input: "{\"key\": [\"value]}",
561 want: "{\"key\": [\"value\"]}",
562 },
563 {
564 name: "embedded_quotes",
565 input: "[\"lorem \"ipsum\" sic\"]",
566 want: "[\"lorem \\\"ipsum\\\" sic\"]",
567 },
568 {
569 name: "array_closes_object",
570 input: "{\"key1\": [\"value1\", \"value2\"}, \"key2\": [\"value3\", \"value4\"]}",
571 want: "{\"key1\": [\"value1\", \"value2\"], \"key2\": [\"value3\", \"value4\"]}",
572 },
573 {
574 name: "rows_missing_bracket",
575 input: "{\"headers\": [\"A\", \"B\", \"C\"], \"rows\": [[\"r1a\", \"r1b\", \"r1c\"], [\"r2a\", \"r2b\", \"r2c\"], \"r3a\", \"r3b\", \"r3c\"], [\"r4a\", \"r4b\", \"r4c\"], [\"r5a\", \"r5b\", \"r5c\"]]}",
576 want: "{\"headers\": [\"A\", \"B\", \"C\"], \"rows\": [[\"r1a\", \"r1b\", \"r1c\"], [\"r2a\", \"r2b\", \"r2c\"], [\"r3a\", \"r3b\", \"r3c\"], [\"r4a\", \"r4b\", \"r4c\"], [\"r5a\", \"r5b\", \"r5c\"]]}",
577 },
578 {
579 name: "array_missing_commas",
580 input: "{\"key\": [\"value\" \"value1\" \"value2\"]}",
581 want: "{\"key\": [\"value\", \"value1\", \"value2\"]}",
582 },
583 {
584 name: "array_many_quotes",
585 input: "{\"key\": [\"lorem \"ipsum\" dolor \"sit\" amet, \"consectetur\" \", \"lorem \"ipsum\" dolor\", \"lorem\"]}",
586 want: "{\"key\": [\"lorem \\\"ipsum\\\" dolor \\\"sit\\\" amet, \\\"consectetur\\\" \", \"lorem \\\"ipsum\\\" dolor\", \"lorem\"]}",
587 },
588 {
589 name: "quoted_key_characters",
590 input: "{\"k\"e\"y\": \"value\"}",
591 want: "{\"k\\\"e\\\"y\": \"value\"}",
592 },
593 {
594 name: "array_object_mixed",
595 input: "[\"key\":\"value\"}]",
596 want: "[{\"key\": \"value\"}]",
597 },
598 {
599 name: "array_object_followed_by_literal",
600 input: "[{\"key\": \"value\", \"key",
601 want: "[{\"key\": \"value\"}, [\"key\"]]",
602 },
603 {
604 name: "set_like_array",
605 input: "{'key1', 'key2'}",
606 want: "[\"key1\", \"key2\"]",
607 },
608 }
609
610 for _, tc := range cases {
611 t.Run(tc.name, func(t *testing.T) {
612 got, err := RepairJSON(tc.input)
613 if err != nil {
614 t.Fatalf("unexpected error: %v", err)
615 }
616 if got != tc.want {
617 t.Fatalf("got %q want %q", got, tc.want)
618 }
619 })
620 }
621}
622
623func TestParseArrayMissingQuotes(t *testing.T) {
624 cases := []struct {
625 name string
626 input string
627 want string
628 }{
629 {
630 name: "value_missing_quote",
631 input: "[\"value1\" value2\", \"value3\"]",
632 want: "[\"value1\", \"value2\", \"value3\"]",
633 },
634 {
635 name: "comment_token",
636 input: "{\"bad_one\":[\"Lorem Ipsum\", \"consectetur\" comment\" ], \"good_one\":[ \"elit\", \"sed\", \"tempor\"]}",
637 want: "{\"bad_one\": [\"Lorem Ipsum\", \"consectetur\", \"comment\"], \"good_one\": [\"elit\", \"sed\", \"tempor\"]}",
638 },
639 {
640 name: "comment_token_no_space",
641 input: "{\"bad_one\": [\"Lorem Ipsum\",\"consectetur\" comment],\"good_one\": [\"elit\",\"sed\",\"tempor\"]}",
642 want: "{\"bad_one\": [\"Lorem Ipsum\", \"consectetur\", \"comment\"], \"good_one\": [\"elit\", \"sed\", \"tempor\"]}",
643 },
644 }
645
646 for _, tc := range cases {
647 t.Run(tc.name, func(t *testing.T) {
648 got, err := RepairJSON(tc.input)
649 if err != nil {
650 t.Fatalf("unexpected error: %v", err)
651 }
652 if got != tc.want {
653 t.Fatalf("got %q want %q", got, tc.want)
654 }
655 })
656 }
657}
658
659func TestParseComment(t *testing.T) {
660 cases := []struct {
661 name string
662 input string
663 want string
664 }{
665 {
666 name: "just_slash",
667 input: "/",
668 want: "",
669 },
670 {
671 name: "block_comment_prefix",
672 input: "/* comment */ {\"key\": \"value\"}",
673 want: "{\"key\": \"value\"}",
674 },
675 {
676 name: "line_comment",
677 input: "{ \"key\": { \"key2\": \"value2\" // comment }, \"key3\": \"value3\" }",
678 want: "{\"key\": {\"key2\": \"value2\"}, \"key3\": \"value3\"}",
679 },
680 {
681 name: "hash_comment",
682 input: "{ \"key\": { \"key2\": \"value2\" # comment }, \"key3\": \"value3\" }",
683 want: "{\"key\": {\"key2\": \"value2\"}, \"key3\": \"value3\"}",
684 },
685 {
686 name: "block_comment_inside",
687 input: "{ \"key\": { \"key2\": \"value2\" /* comment */ }, \"key3\": \"value3\" }",
688 want: "{\"key\": {\"key2\": \"value2\"}, \"key3\": \"value3\"}",
689 },
690 {
691 name: "array_block_comment",
692 input: "[ \"value\", /* comment */ \"value2\" ]",
693 want: "[\"value\", \"value2\"]",
694 },
695 {
696 name: "unterminated_comment",
697 input: "{ \"key\": \"value\" /* comment",
698 want: "{\"key\": \"value\"}",
699 },
700 }
701
702 for _, tc := range cases {
703 t.Run(tc.name, func(t *testing.T) {
704 got, err := RepairJSON(tc.input)
705 if err != nil {
706 t.Fatalf("unexpected error: %v", err)
707 }
708 if got != tc.want {
709 t.Fatalf("got %q want %q", got, tc.want)
710 }
711 })
712 }
713}
714
715func TestParseNumber(t *testing.T) {
716 cases := []struct {
717 name string
718 input string
719 want any
720 }{
721 {
722 name: "integer",
723 input: "1",
724 want: json.Number("1"),
725 },
726 {
727 name: "float",
728 input: "1.2",
729 want: json.Number("1.2"),
730 },
731 {
732 name: "underscored_integer",
733 input: "{\"value\": 82_461_110}",
734 want: map[string]any{
735 "value": json.Number("82461110"),
736 },
737 },
738 {
739 name: "underscored_float",
740 input: "{\"value\": 1_234.5_6}",
741 want: map[string]any{
742 "value": json.Number("1234.56"),
743 },
744 },
745 }
746
747 for _, tc := range cases {
748 t.Run(tc.name, func(t *testing.T) {
749 got, err := Loads(tc.input)
750 if err != nil {
751 t.Fatalf("unexpected error: %v", err)
752 }
753 if !reflect.DeepEqual(got, tc.want) {
754 t.Fatalf("got %#v want %#v", got, tc.want)
755 }
756 })
757 }
758}
759
760func TestParseNumberEdgeCases(t *testing.T) {
761 cases := []struct {
762 name string
763 input string
764 want string
765 }{
766 {
767 name: "leading_dash",
768 input: " - { \"test_key\": [\"test_value\", \"test_value2\"] }",
769 want: "{\"test_key\": [\"test_value\", \"test_value2\"]}",
770 },
771 {
772 name: "fraction",
773 input: "{\"key\": 1/3}",
774 want: "{\"key\": \"1/3\"}",
775 },
776 {
777 name: "leading_decimal",
778 input: "{\"key\": .25}",
779 want: "{\"key\": 0.25}",
780 },
781 {
782 name: "fraction_in_object",
783 input: "{\"here\": \"now\", \"key\": 1/3, \"foo\": \"bar\"}",
784 want: "{\"here\": \"now\", \"key\": \"1/3\", \"foo\": \"bar\"}",
785 },
786 {
787 name: "fraction_long",
788 input: "{\"key\": 12345/67890}",
789 want: "{\"key\": \"12345/67890\"}",
790 },
791 {
792 name: "array_incomplete",
793 input: "[105,12",
794 want: "[105, 12]",
795 },
796 {
797 name: "object_numbers",
798 input: "{\"key\", 105,12,",
799 want: "{\"key\": \"105,12\"}",
800 },
801 {
802 name: "fraction_trailing",
803 input: "{\"key\": 1/3, \"foo\": \"bar\"}",
804 want: "{\"key\": \"1/3\", \"foo\": \"bar\"}",
805 },
806 {
807 name: "dash_number",
808 input: "{\"key\": 10-20}",
809 want: "{\"key\": \"10-20\"}",
810 },
811 {
812 name: "double_dot",
813 input: "{\"key\": 1.1.1}",
814 want: "{\"key\": \"1.1.1\"}",
815 },
816 {
817 name: "dash_array",
818 input: "[- ",
819 want: "[]",
820 },
821 {
822 name: "trailing_decimal",
823 input: "{\"key\": 1. }",
824 want: "{\"key\": 1.0}",
825 },
826 {
827 name: "exponent",
828 input: "{\"key\": 1e10 }",
829 want: "{\"key\": 10000000000.0}",
830 },
831 {
832 name: "bad_exponent",
833 input: "{\"key\": 1e }",
834 want: "{\"key\": 1}",
835 },
836 {
837 name: "non_number_suffix",
838 input: "{\"key\": 1notanumber }",
839 want: "{\"key\": \"1notanumber\"}",
840 },
841 {
842 name: "uuid_literal",
843 input: "{\"rowId\": 57eeeeb1-450b-482c-81b9-4be77e95dee2}",
844 want: "{\"rowId\": \"57eeeeb1-450b-482c-81b9-4be77e95dee2\"}",
845 },
846 {
847 name: "array_non_number",
848 input: "[1, 2notanumber]",
849 want: "[1, \"2notanumber\"]",
850 },
851 }
852
853 for _, tc := range cases {
854 t.Run(tc.name, func(t *testing.T) {
855 got, err := RepairJSON(tc.input)
856 if err != nil {
857 t.Fatalf("unexpected error: %v", err)
858 }
859 if got != tc.want {
860 t.Fatalf("got %q want %q", got, tc.want)
861 }
862 })
863 }
864}
865
866func TestParseObjectObjects(t *testing.T) {
867 cases := []struct {
868 name string
869 input string
870 want any
871 }{
872 {
873 name: "empty_object",
874 input: "{}",
875 want: map[string]any{},
876 },
877 {
878 name: "object_values",
879 input: "{ \"key\": \"value\", \"key2\": 1, \"key3\": True }",
880 want: map[string]any{
881 "key": "value",
882 "key2": json.Number("1"),
883 "key3": true,
884 },
885 },
886 {
887 name: "unfinished_object",
888 input: "{",
889 want: map[string]any{},
890 },
891 {
892 name: "object_with_literals",
893 input: "{ \"key\": value, \"key2\": 1 \"key3\": null }",
894 want: map[string]any{
895 "key": "value",
896 "key2": json.Number("1"),
897 "key3": nil,
898 },
899 },
900 }
901
902 for _, tc := range cases {
903 t.Run(tc.name, func(t *testing.T) {
904 got, err := Loads(tc.input)
905 if err != nil {
906 t.Fatalf("unexpected error: %v", err)
907 }
908 if !reflect.DeepEqual(got, tc.want) {
909 t.Fatalf("got %#v want %#v", got, tc.want)
910 }
911 })
912 }
913}
914
915func TestParseObjectEdgeCases(t *testing.T) {
916 cases := []struct {
917 name string
918 input string
919 want string
920 }{
921 {
922 name: "empty_trim",
923 input: " { } ",
924 want: "{}",
925 },
926 {
927 name: "just_open",
928 input: "{",
929 want: "{}",
930 },
931 {
932 name: "just_close",
933 input: "}",
934 want: "",
935 },
936 {
937 name: "dangling_quote",
938 input: "{\"",
939 want: "{}",
940 },
941 {
942 name: "object_array_merge",
943 input: "{foo: [}",
944 want: "{\"foo\": []}",
945 },
946 {
947 name: "empty_key",
948 input: "{\"\": \"value\"",
949 want: "{\"\": \"value\"}",
950 },
951 {
952 name: "embedded_quotes",
953 input: "{\"key\": \"v\"alue\"}",
954 want: "{\"key\": \"v\\\"alue\\\"\"}",
955 },
956 {
957 name: "comment_literal",
958 input: "{\"value_1\": true, COMMENT \"value_2\": \"data\"}",
959 want: "{\"value_1\": true, \"value_2\": \"data\"}",
960 },
961 {
962 name: "comment_literal_trailing",
963 input: "{\"value_1\": true, SHOULD_NOT_EXIST \"value_2\": \"data\" AAAA }",
964 want: "{\"value_1\": true, \"value_2\": \"data\"}",
965 },
966 {
967 name: "empty_key_bool",
968 input: "{\"\" : true, \"key2\": \"value2\"}",
969 want: "{\"\": true, \"key2\": \"value2\"}",
970 },
971 {
972 name: "double_quotes",
973 input: "{\"\"answer\"\":[{\"\"traits\"\":''Female aged 60+'',\"\"answer1\"\":\"\"5\"\"}]}",
974 want: "{\"answer\": [{\"traits\": \"Female aged 60+\", \"answer1\": \"5\"}]}",
975 },
976 {
977 name: "missing_quotes",
978 input: "{ \"words\": abcdef\", \"numbers\": 12345\", \"words2\": ghijkl\" }",
979 want: "{\"words\": \"abcdef\", \"numbers\": 12345, \"words2\": \"ghijkl\"}",
980 },
981 {
982 name: "broken_split_key",
983 input: "{\"number\": 1,\"reason\": \"According...\"\"ans\": \"YES\"}",
984 want: "{\"number\": 1, \"reason\": \"According...\", \"ans\": \"YES\"}",
985 },
986 {
987 name: "nested_braces_in_string",
988 input: "{ \"a\" : \"{ b\": {} }\" }",
989 want: "{\"a\": \"{ b\"}",
990 },
991 {
992 name: "literal_after_string",
993 input: "{\"b\": \"xxxxx\" true}",
994 want: "{\"b\": \"xxxxx\"}",
995 },
996 {
997 name: "string_with_quotes",
998 input: "{\"key\": \"Lorem \"ipsum\" s,\"}",
999 want: "{\"key\": \"Lorem \\\"ipsum\\\" s,\"}",
1000 },
1001 {
1002 name: "literal_list",
1003 input: "{\"lorem\": ipsum, sic, datum.\",}",
1004 want: "{\"lorem\": \"ipsum, sic, datum.\"}",
1005 },
1006 {
1007 name: "multiple_keys",
1008 input: "{\"lorem\": sic tamet. \"ipsum\": sic tamet, quick brown fox. \"sic\": ipsum}",
1009 want: "{\"lorem\": \"sic tamet.\", \"ipsum\": \"sic tamet\", \"sic\": \"ipsum\"}",
1010 },
1011 {
1012 name: "unfinished_string",
1013 input: "{\"lorem_ipsum\": \"sic tamet, quick brown fox. }",
1014 want: "{\"lorem_ipsum\": \"sic tamet, quick brown fox.\"}",
1015 },
1016 {
1017 name: "missing_quotes_keys",
1018 input: "{\"key\":value, \" key2\":\"value2\" }",
1019 want: "{\"key\": \"value\", \" key2\": \"value2\"}",
1020 },
1021 {
1022 name: "missing_quotes_key_separator",
1023 input: "{\"key\":value \"key2\":\"value2\" }",
1024 want: "{\"key\": \"value\", \"key2\": \"value2\"}",
1025 },
1026 {
1027 name: "single_quotes_braces",
1028 input: "{'text': 'words{words in brackets}more words'}",
1029 want: "{\"text\": \"words{words in brackets}more words\"}",
1030 },
1031 {
1032 name: "literal_with_braces",
1033 input: "{text:words{words in brackets}}",
1034 want: "{\"text\": \"words{words in brackets}\"}",
1035 },
1036 {
1037 name: "literal_with_braces_suffix",
1038 input: "{text:words{words in brackets}m}",
1039 want: "{\"text\": \"words{words in brackets}m\"}",
1040 },
1041 {
1042 name: "trailing_markdown",
1043 input: "{\"key\": \"value, value2\"```",
1044 want: "{\"key\": \"value, value2\"}",
1045 },
1046 {
1047 name: "trailing_markdown_quote",
1048 input: "{\"key\": \"value}```",
1049 want: "{\"key\": \"value\"}",
1050 },
1051 {
1052 name: "bare_keys",
1053 input: "{key:value,key2:value2}",
1054 want: "{\"key\": \"value\", \"key2\": \"value2\"}",
1055 },
1056 {
1057 name: "missing_key_quote",
1058 input: "{\"key:\"value\"}",
1059 want: "{\"key\": \"value\"}",
1060 },
1061 {
1062 name: "missing_value_quote",
1063 input: "{\"key:value}",
1064 want: "{\"key\": \"value\"}",
1065 },
1066 {
1067 name: "array_double_quotes",
1068 input: "[{\"lorem\": {\"ipsum\": \"sic\"}, \"\"\"\" \"lorem\": {\"ipsum\": \"sic\"}]",
1069 want: "[{\"lorem\": {\"ipsum\": \"sic\"}}, {\"lorem\": {\"ipsum\": \"sic\"}}]",
1070 },
1071 {
1072 name: "arrays_in_object",
1073 input: "{ \"key\": [\"arrayvalue\"], [\"arrayvalue1\"], [\"arrayvalue2\"], \"key3\": \"value3\" }",
1074 want: "{\"key\": [\"arrayvalue\", \"arrayvalue1\", \"arrayvalue2\"], \"key3\": \"value3\"}",
1075 },
1076 {
1077 name: "nested_arrays_in_object",
1078 input: "{ \"key\": [[1, 2, 3], \"a\", \"b\"], [[4, 5, 6], [7, 8, 9]] }",
1079 want: "{\"key\": [[1, 2, 3], \"a\", \"b\", [4, 5, 6], [7, 8, 9]]}",
1080 },
1081 {
1082 name: "array_key_missing_value",
1083 input: "{ \"key\": [\"arrayvalue\"], \"key3\": \"value3\", [\"arrayvalue1\"] }",
1084 want: "{\"key\": [\"arrayvalue\"], \"key3\": \"value3\", \"arrayvalue1\": \"\"}",
1085 },
1086 {
1087 name: "json_string_literal",
1088 input: "{\"key\": \"{\\\"key\\\":[\\\"value\\\"],\\\"key2\\\":\"value2\"}\"}",
1089 want: "{\"key\": \"{\\\"key\\\":[\\\"value\\\"],\\\"key2\\\":\\\"value2\\\"}\"}",
1090 },
1091 {
1092 name: "empty_value",
1093 input: "{\"key\": , \"key2\": \"value2\"}",
1094 want: "{\"key\": \"\", \"key2\": \"value2\"}",
1095 },
1096 {
1097 name: "array_missing_object_end",
1098 input: "{\"array\":[{\"key\": \"value\"], \"key2\": \"value2\"}",
1099 want: "{\"array\": [{\"key\": \"value\"}], \"key2\": \"value2\"}",
1100 },
1101 {
1102 name: "object_double_close",
1103 input: "[{\"key\":\"value\"}},{\"key\":\"value\"}]",
1104 want: "[{\"key\": \"value\"}, {\"key\": \"value\"}]",
1105 },
1106 }
1107
1108 for _, tc := range cases {
1109 t.Run(tc.name, func(t *testing.T) {
1110 got, err := RepairJSON(tc.input)
1111 if err != nil {
1112 t.Fatalf("unexpected error: %v", err)
1113 }
1114 if got != tc.want {
1115 t.Fatalf("got %q want %q", got, tc.want)
1116 }
1117 })
1118 }
1119}
1120
1121func TestParseObjectMergeAtEnd(t *testing.T) {
1122 cases := []struct {
1123 name string
1124 input string
1125 want string
1126 }{
1127 {
1128 name: "merge_key",
1129 input: "{\"key\": \"value\"}, \"key2\": \"value2\"}",
1130 want: "{\"key\": \"value\", \"key2\": \"value2\"}",
1131 },
1132 {
1133 name: "merge_empty_value",
1134 input: "{\"key\": \"value\"}, \"key2\": }",
1135 want: "{\"key\": \"value\", \"key2\": \"\"}",
1136 },
1137 {
1138 name: "merge_array_discard",
1139 input: "{\"key\": \"value\"}, []",
1140 want: "{\"key\": \"value\"}",
1141 },
1142 {
1143 name: "merge_array_keep",
1144 input: "{\"key\": \"value\"}, [\"abc\"]",
1145 want: "[{\"key\": \"value\"}, [\"abc\"]]",
1146 },
1147 {
1148 name: "merge_object",
1149 input: "{\"key\": \"value\"}, {}",
1150 want: "{\"key\": \"value\"}",
1151 },
1152 {
1153 name: "merge_empty_key",
1154 input: "{\"key\": \"value\"}, \"\" : \"value2\"}",
1155 want: "{\"key\": \"value\", \"\": \"value2\"}",
1156 },
1157 {
1158 name: "merge_missing_colon",
1159 input: "{\"key\": \"value\"}, \"key2\" \"value2\"}",
1160 want: "{\"key\": \"value\", \"key2\": \"value2\"}",
1161 },
1162 {
1163 name: "merge_multiple_keys",
1164 input: "{\"key1\": \"value1\"}, \"key2\": \"value2\", \"key3\": \"value3\"}",
1165 want: "{\"key1\": \"value1\", \"key2\": \"value2\", \"key3\": \"value3\"}",
1166 },
1167 }
1168
1169 for _, tc := range cases {
1170 t.Run(tc.name, func(t *testing.T) {
1171 got, err := RepairJSON(tc.input)
1172 if err != nil {
1173 t.Fatalf("unexpected error: %v", err)
1174 }
1175 if got != tc.want {
1176 t.Fatalf("got %q want %q", got, tc.want)
1177 }
1178 })
1179 }
1180}
1181
1182func TestParseStringBasics(t *testing.T) {
1183 cases := []struct {
1184 name string
1185 input string
1186 want string
1187 }{
1188 {
1189 name: "just_quote",
1190 input: "\"",
1191 want: "",
1192 },
1193 {
1194 name: "newline",
1195 input: "\n",
1196 want: "",
1197 },
1198 {
1199 name: "space",
1200 input: " ",
1201 want: "",
1202 },
1203 {
1204 name: "string_literal",
1205 input: "string",
1206 want: "",
1207 },
1208 {
1209 name: "string_before_object",
1210 input: "stringbeforeobject {}",
1211 want: "{}",
1212 },
1213 }
1214
1215 for _, tc := range cases {
1216 t.Run(tc.name, func(t *testing.T) {
1217 got, err := RepairJSON(tc.input)
1218 if err != nil {
1219 t.Fatalf("unexpected error: %v", err)
1220 }
1221 if got != tc.want {
1222 t.Fatalf("got %q want %q", got, tc.want)
1223 }
1224 })
1225 }
1226}
1227
1228func TestMissingAndMixedQuotes(t *testing.T) {
1229 cases := []struct {
1230 name string
1231 input string
1232 want string
1233 }{
1234 {
1235 name: "mixed_quotes",
1236 input: "{'key': 'string', 'key2': false, \"key3\": null, \"key4\": unquoted}",
1237 want: "{\"key\": \"string\", \"key2\": false, \"key3\": null, \"key4\": \"unquoted\"}",
1238 },
1239 {
1240 name: "missing_last_quote",
1241 input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York",
1242 want: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
1243 },
1244 {
1245 name: "missing_quotes_key",
1246 input: "{\"name\": \"John\", \"age\": 30, city: \"New York\"}",
1247 want: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
1248 },
1249 {
1250 name: "missing_quotes_value",
1251 input: "{\"name\": \"John\", \"age\": 30, \"city\": New York}",
1252 want: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
1253 },
1254 {
1255 name: "missing_quotes_value_with_name",
1256 input: "{\"name\": John, \"age\": 30, \"city\": \"New York\"}",
1257 want: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
1258 },
1259 {
1260 name: "slanted_delimiter",
1261 input: "{“slanted_delimiter”: \"value\"}",
1262 want: "{\"slanted_delimiter\": \"value\"}",
1263 },
1264 {
1265 name: "shortened_string",
1266 input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New",
1267 want: "{\"name\": \"John\", \"age\": 30, \"city\": \"New\"}",
1268 },
1269 {
1270 name: "missing_quote_in_middle",
1271 input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York, \"gender\": \"male\"}",
1272 want: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\", \"gender\": \"male\"}",
1273 },
1274 {
1275 name: "comment_literal_in_array",
1276 input: "[{\"key\": \"value\", COMMENT \"notes\": \"lorem \"ipsum\", sic.\" }]",
1277 want: "[{\"key\": \"value\", \"notes\": \"lorem \\\"ipsum\\\", sic.\"}]",
1278 },
1279 {
1280 name: "double_quote_prefix",
1281 input: "{\"key\": \"\"value\"}",
1282 want: "{\"key\": \"value\"}",
1283 },
1284 {
1285 name: "numeric_key",
1286 input: "{\"key\": \"value\", 5: \"value\"}",
1287 want: "{\"key\": \"value\", \"5\": \"value\"}",
1288 },
1289 {
1290 name: "escaped_quotes",
1291 input: "{\"foo\": \"\\\\\"bar\\\\\"\"",
1292 want: "{\"foo\": \"\\\"bar\\\"\"}",
1293 },
1294 {
1295 name: "empty_key_prefix",
1296 input: "{\"\" key\":\"val\"",
1297 want: "{\" key\": \"val\"}",
1298 },
1299 {
1300 name: "missing_comma",
1301 input: "{\"key\": value \"key2\" : \"value2\" ",
1302 want: "{\"key\": \"value\", \"key2\": \"value2\"}",
1303 },
1304 {
1305 name: "ellipsis_quotes",
1306 input: "{\"key\": \"lorem ipsum ... \"sic \" tamet. ...}",
1307 want: "{\"key\": \"lorem ipsum ... \\\"sic \\\" tamet. ...\"}",
1308 },
1309 {
1310 name: "trailing_comma",
1311 input: "{\"key\": value , }",
1312 want: "{\"key\": \"value\"}",
1313 },
1314 {
1315 name: "comment_in_string",
1316 input: "{\"comment\": \"lorem, \"ipsum\" sic \"tamet\". To improve\"}",
1317 want: "{\"comment\": \"lorem, \\\"ipsum\\\" sic \\\"tamet\\\". To improve\"}",
1318 },
1319 {
1320 name: "value_with_embedded_quotes",
1321 input: "{\"key\": \"v\"alu\"e\"} key:",
1322 want: "{\"key\": \"v\\\"alu\\\"e\"}",
1323 },
1324 {
1325 name: "value_with_embedded_quote",
1326 input: "{\"key\": \"v\"alue\", \"key2\": \"value2\"}",
1327 want: "{\"key\": \"v\\\"alue\", \"key2\": \"value2\"}",
1328 },
1329 {
1330 name: "array_value_with_quote",
1331 input: "[{\"key\": \"v\"alu,e\", \"key2\": \"value2\"}]",
1332 want: "[{\"key\": \"v\\\"alu,e\", \"key2\": \"value2\"}]",
1333 },
1334 }
1335
1336 for _, tc := range cases {
1337 t.Run(tc.name, func(t *testing.T) {
1338 got, err := RepairJSON(tc.input)
1339 if err != nil {
1340 t.Fatalf("unexpected error: %v", err)
1341 }
1342 if got != tc.want {
1343 t.Fatalf("got %q want %q", got, tc.want)
1344 }
1345 })
1346 }
1347}
1348
1349func TestEscaping(t *testing.T) {
1350 cases := []struct {
1351 name string
1352 input string
1353 want string
1354 }{
1355 {
1356 name: "just_quotes",
1357 input: "'\"'",
1358 want: "",
1359 },
1360 {
1361 name: "escaped_chars",
1362 input: "{\"key\": 'string\"\n\t\\\\le'",
1363 want: "{\"key\": \"string\\\"\\n\\t\\\\le\"}",
1364 },
1365 {
1366 name: "html_escape",
1367 input: "{\"real_content\": \"Some string: Some other string \\t Some string <a href=\\\"https://domain.com\\\">Some link</a>\"",
1368 want: "{\"real_content\": \"Some string: Some other string \\t Some string <a href=\\\"https://domain.com\\\">Some link</a>\"}",
1369 },
1370 {
1371 name: "newline_in_key",
1372 input: "{\"key_1\n\": \"value\"}",
1373 want: "{\"key_1\": \"value\"}",
1374 },
1375 {
1376 name: "tab_in_key",
1377 input: "{\"key\t_\": \"value\"}",
1378 want: "{\"key\\t_\": \"value\"}",
1379 },
1380 {
1381 name: "unicode_escape",
1382 input: "{\"key\": '\u0076\u0061\u006c\u0075\u0065'}",
1383 want: "{\"key\": \"value\"}",
1384 },
1385 {
1386 name: "unicode_escape_skip_loads",
1387 input: "{\"key\": \"\\u0076\\u0061\\u006C\\u0075\\u0065\"}",
1388 want: "{\"key\": \"value\"}",
1389 },
1390 {
1391 name: "escaped_single_quote",
1392 input: "{\"key\": \"valu\\'e\"}",
1393 want: "{\"key\": \"valu'e\"}",
1394 },
1395 {
1396 name: "escaped_object",
1397 input: "{'key': \"{\\\"key\\\": 1, \\\"key2\\\": 1}\"}",
1398 want: "{\"key\": \"{\\\"key\\\": 1, \\\"key2\\\": 1}\"}",
1399 },
1400 }
1401
1402 for _, tc := range cases {
1403 t.Run(tc.name, func(t *testing.T) {
1404 opts := []Option{}
1405 if tc.name == "unicode_escape_skip_loads" {
1406 opts = append(opts, WithSkipJSONLoads())
1407 }
1408 got, err := RepairJSON(tc.input, opts...)
1409 if err != nil {
1410 t.Fatalf("unexpected error: %v", err)
1411 }
1412 if got != tc.want {
1413 t.Fatalf("got %q want %q", got, tc.want)
1414 }
1415 })
1416 }
1417}
1418
1419func TestMarkdown(t *testing.T) {
1420 cases := []struct {
1421 name string
1422 input string
1423 want string
1424 }{
1425 {
1426 name: "markdown_link",
1427 input: "{ \"content\": \"[LINK](\"https://google.com\")\" }",
1428 want: "{\"content\": \"[LINK](\\\"https://google.com\\\")\"}",
1429 },
1430 {
1431 name: "markdown_incomplete",
1432 input: "{ \"content\": \"[LINK](\" }",
1433 want: "{\"content\": \"[LINK](\"}",
1434 },
1435 {
1436 name: "markdown_in_object",
1437 input: "{ \"content\": \"[LINK](\", \"key\": true }",
1438 want: "{\"content\": \"[LINK](\", \"key\": true}",
1439 },
1440 }
1441
1442 for _, tc := range cases {
1443 t.Run(tc.name, func(t *testing.T) {
1444 got, err := RepairJSON(tc.input)
1445 if err != nil {
1446 t.Fatalf("unexpected error: %v", err)
1447 }
1448 if got != tc.want {
1449 t.Fatalf("got %q want %q", got, tc.want)
1450 }
1451 })
1452 }
1453}
1454
1455func TestLeadingTrailingCharacters(t *testing.T) {
1456 cases := []struct {
1457 name string
1458 input string
1459 want string
1460 }{
1461 {
1462 name: "wrapped_markdown",
1463 input: "````{ \"key\": \"value\" }```",
1464 want: "{\"key\": \"value\"}",
1465 },
1466 {
1467 name: "trailing_markdown_block",
1468 input: "{ \"a\": \"\", \"b\": [ { \"c\": 1} ] \n}```",
1469 want: "{\"a\": \"\", \"b\": [{\"c\": 1}]}",
1470 },
1471 {
1472 name: "preface_text",
1473 input: "Based on the information extracted, here is the filled JSON output: ```json { 'a': 'b' } ```",
1474 want: "{\"a\": \"b\"}",
1475 },
1476 {
1477 name: "multiline_markdown",
1478 input: "\n The next 64 elements are:\n ```json\n { \"key\": \"value\" }\n ```",
1479 want: "{\"key\": \"value\"}",
1480 },
1481 }
1482
1483 for _, tc := range cases {
1484 t.Run(tc.name, func(t *testing.T) {
1485 got, err := RepairJSON(tc.input)
1486 if err != nil {
1487 t.Fatalf("unexpected error: %v", err)
1488 }
1489 if got != tc.want {
1490 t.Fatalf("got %q want %q", got, tc.want)
1491 }
1492 })
1493 }
1494}
1495
1496func TestStringJSONLLMBlock(t *testing.T) {
1497 cases := []struct {
1498 name string
1499 input string
1500 want string
1501 }{
1502 {
1503 name: "backticks",
1504 input: "{\"key\": \"``\"",
1505 want: "{\"key\": \"``\"}",
1506 },
1507 {
1508 name: "backticks_json",
1509 input: "{\"key\": \"```json\"",
1510 want: "{\"key\": \"```json\"}",
1511 },
1512 {
1513 name: "json_block_inside_string",
1514 input: "{\"key\": \"```json {\"key\": [{\"key1\": 1},{\"key2\": 2}]}```\"}",
1515 want: "{\"key\": {\"key\": [{\"key1\": 1}, {\"key2\": 2}]}}",
1516 },
1517 {
1518 name: "response_prefix",
1519 input: "{\"response\": \"```json{}\"",
1520 want: "{\"response\": \"```json{}\"}",
1521 },
1522 }
1523
1524 for _, tc := range cases {
1525 t.Run(tc.name, func(t *testing.T) {
1526 got, err := RepairJSON(tc.input)
1527 if err != nil {
1528 t.Fatalf("unexpected error: %v", err)
1529 }
1530 if got != tc.want {
1531 t.Fatalf("got %q want %q", got, tc.want)
1532 }
1533 })
1534 }
1535}
1536
1537func TestParseBooleanOrNull(t *testing.T) {
1538 loadCases := []struct {
1539 name string
1540 input string
1541 want any
1542 }{
1543 {
1544 name: "upper_true",
1545 input: "True",
1546 want: "",
1547 },
1548 {
1549 name: "upper_false",
1550 input: "False",
1551 want: "",
1552 },
1553 {
1554 name: "upper_null",
1555 input: "Null",
1556 want: "",
1557 },
1558 {
1559 name: "lower_true",
1560 input: "true",
1561 want: true,
1562 },
1563 {
1564 name: "lower_false",
1565 input: "false",
1566 want: false,
1567 },
1568 {
1569 name: "lower_null",
1570 input: "null",
1571 want: nil,
1572 },
1573 }
1574
1575 for _, tc := range loadCases {
1576 t.Run(tc.name, func(t *testing.T) {
1577 got, err := Loads(tc.input)
1578 if err != nil {
1579 t.Fatalf("unexpected error: %v", err)
1580 }
1581 if !reflect.DeepEqual(got, tc.want) {
1582 t.Fatalf("got %#v want %#v", got, tc.want)
1583 }
1584 })
1585 }
1586
1587 stringCases := []struct {
1588 name string
1589 input string
1590 want string
1591 }{
1592 {
1593 name: "bools_in_object",
1594 input: " {\"key\": true, \"key2\": false, \"key3\": null}",
1595 want: "{\"key\": true, \"key2\": false, \"key3\": null}",
1596 },
1597 {
1598 name: "uppercase_bools",
1599 input: "{\"key\": TRUE, \"key2\": FALSE, \"key3\": Null} ",
1600 want: "{\"key\": true, \"key2\": false, \"key3\": null}",
1601 },
1602 }
1603
1604 for _, tc := range stringCases {
1605 t.Run(tc.name, func(t *testing.T) {
1606 got, err := RepairJSON(tc.input)
1607 if err != nil {
1608 t.Fatalf("unexpected error: %v", err)
1609 }
1610 if got != tc.want {
1611 t.Fatalf("got %q want %q", got, tc.want)
1612 }
1613 })
1614 }
1615}