Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 52 additions & 25 deletions gs/encoding/json/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2122,28 +2122,60 @@ function assignDecodedValue(
}
}

// decodedFieldKey returns the key of the decoded object for the field with
// the given JSON name, of the fields with the given names, or undefined if
// it has none. As in Go, a key matches the name of a field without case,
// unless another field has that exact name, and the last key that matches
// is the one that is used, as the later keys overwrite the earlier ones.
function decodedFieldKey(
decoded: Record<string, unknown>,
jsonName: string,
names: Set<string>,
): string | undefined {
const folded = jsonName.toLowerCase()
let found: string | undefined
for (const key of Object.keys(decoded)) {
if (key === jsonName || (!names.has(key) && key.toLowerCase() === folded)) {
found = key
}
}
return found
}

// checkUnknownFields returns an error, for DisallowUnknownFields, if the
// decoded object has a key that matches none of the given names of the
// fields, without case.
function checkUnknownFields(
decoded: Record<string, unknown>,
names: Set<string>,
): void {
const folded = new Set([...names].map((name) => name.toLowerCase()))
for (const key of Object.keys(decoded)) {
if (!names.has(key) && !folded.has(key.toLowerCase())) {
throw $.newError(`json: unknown field "${key}"`)
}
}
}

function assignStructFields(
target: Record<string, unknown>,
decoded: Record<string, unknown>,
opts: decodeOptions,
): void {
const fields = jsonStructFields(target)
const names = new Set(fields.map((field) => field.jsonName))
if (opts.disallowUnknownFields) {
const knownNames = new Set(fields.map((field) => field.jsonName))
for (const key of Object.keys(decoded)) {
if (!knownNames.has(key)) {
throw $.newError(`json: unknown field "${key}"`)
}
}
checkUnknownFields(decoded, names)
}
for (const { jsonName, owner, field } of fields) {
if (!(field.key in owner)) {
continue
}
if (Object.prototype.hasOwnProperty.call(decoded, jsonName)) {
const key = decodedFieldKey(decoded, jsonName, names)
if (key !== undefined) {
assignDecodedFieldValue(
$.fieldRef(owner, field.key),
decoded[jsonName],
decoded[key],
opts,
field.type,
)
Expand Down Expand Up @@ -2804,26 +2836,21 @@ function assignAnonymousStructFields(
fields: fieldMetadata[],
opts: decodeOptions,
): void {
if (opts.disallowUnknownFields) {
const knownNames = new Set<string>()
for (const field of fields) {
const jsonName = jsonFieldName(field.name, field.tag)
if (jsonName !== '') {
knownNames.add(jsonName)
}
}
for (const key of Object.keys(decoded)) {
if (!knownNames.has(key)) {
throw $.newError(`json: unknown field "${key}"`)
}
const names = new Set<string>()
for (const field of fields) {
const jsonName = jsonFieldName(field.name, field.tag)
if (jsonName !== '') {
names.add(jsonName)
}
}
if (opts.disallowUnknownFields) {
checkUnknownFields(decoded, names)
}
for (const field of fields) {
const jsonName = jsonFieldName(field.name, field.tag)
if (
jsonName === '' ||
!Object.prototype.hasOwnProperty.call(decoded, jsonName)
) {
const key =
jsonName === '' ? undefined : decodedFieldKey(decoded, jsonName, names)
if (key === undefined) {
continue
}
// An anonymous struct's own fields start unset (the target object
Expand All @@ -2836,7 +2863,7 @@ function assignAnonymousStructFields(
// type (a bare type-name spelling resolved through $.getTypeByName);
// an inline TypeInfo with no ctor keeps the existing generic path
// unchanged below.
const fieldValue = decoded[jsonName]
const fieldValue = decoded[key]
const fieldTypeInfo = resolveTypeInfo(field.type)
if (
fieldTypeInfo !== null &&
Expand Down
5 changes: 5 additions & 0 deletions tests/tests/json_simple/expected.log
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
Result: {"x":42}
named: true abc commit main
anonymous: true def
both cases: true y t
unknown: true
known: true w
2 changes: 1 addition & 1 deletion tests/tests/json_simple/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { Simple } from "./json_simple.gs.ts"
export { Object, Ref, Simple } from "./json_simple.gs.ts"
40 changes: 40 additions & 0 deletions tests/tests/json_simple/json_simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"encoding/json"
"strings"
)

type Simple struct {
Expand All @@ -16,4 +17,43 @@ func main() {
} else {
println("Result:", string(b))
}
unmarshalNames()
}

type Ref struct {
Object Object
Label string `json:"label"`
}

type Object struct {
SHA string
Type string
}

// unmarshalNames tests that Unmarshal matches the names of the fields
// without case, preferring the same case, as for the lowercase names of an
// API, including in anonymous structs and with DisallowUnknownFields.
func unmarshalNames() {
var r Ref
err := json.Unmarshal([]byte(`{"object":{"sha":"abc","TYPE":"commit"},"LABEL":"main"}`), &r)
println("named:", err == nil, r.Object.SHA, r.Object.Type, r.Label)

var a struct {
Object struct{ SHA string }
}
err = json.Unmarshal([]byte(`{"object":{"sha":"def"}}`), &a)
println("anonymous:", err == nil, a.Object.SHA)

var o Object
err = json.Unmarshal([]byte(`{"sha":"x","SHA":"y","type":"t"}`), &o)
println("both cases:", err == nil, o.SHA, o.Type)

d := json.NewDecoder(strings.NewReader(`{"sha":"z","other":1}`))
d.DisallowUnknownFields()
err = d.Decode(&o)
println("unknown:", err != nil)
d = json.NewDecoder(strings.NewReader(`{"sha":"w"}`))
d.DisallowUnknownFields()
err = d.Decode(&o)
println("known:", err == nil, o.SHA)
}
91 changes: 91 additions & 0 deletions tests/tests/json_simple/json_simple.gs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import * as $ from "@goscript/builtin/index.js"
import * as json from "@goscript/encoding/json/index.js"
import * as strings from "@goscript/strings/index.js"
import type * as io from "@goscript/io/index.js"

export class Simple {
public declare X: number
Expand Down Expand Up @@ -30,6 +32,66 @@ export class Simple {
)
}

export class Object {
public declare SHA: string

public declare Type: string

constructor(__goscriptValue?: boolean, SHA$?: string, Type$?: string) {
this.SHA = SHA$ ?? ""
this.Type = Type$ ?? ""
;(this as any).__goStructValue = __goscriptValue === true
}

public clone(): Object {
return new Object(true, this.SHA, this.Type)
}

public __goscriptAssign(source: Object): this {
this.SHA = source.SHA
this.Type = source.Type
return this
}

static __typeInfo = $.registerStructType(
"main.Object",
() => new Object(),
() => [],
Object,
() => [{ name: "SHA", key: "SHA", type: /* @__PURE__ */ $.basicType("string") }, { name: "Type", key: "Type", type: /* @__PURE__ */ $.basicType("string") }]
)
}

export class Ref {
public declare Object: Object

public declare Label: string

constructor(__goscriptValue?: boolean, Object$?: Object, Label$?: string) {
this.Object = Object$ ?? new Object(true)
this.Label = Label$ ?? ""
;(this as any).__goStructValue = __goscriptValue === true
}

public clone(): Ref {
return new Ref(true, this.Object.clone(), this.Label)
}

public __goscriptAssign(source: Ref): this {
this.Object.__goscriptAssign(source.Object)
this.Label = source.Label
return this
}

static __typeInfo = $.registerStructType(
"main.Ref",
() => new Ref(),
() => [],
Ref,
() => [{ name: "Object", key: "Object", type: "main.Object" }, { name: "Label", key: "Label", type: /* @__PURE__ */ $.basicType("string"), tag: "json:\"label\"" }]
)
}

export async function main(): globalThis.Promise<void> {
let s = new Simple(true, 42)
let __goscriptTuple0: any = (json.Marshal($.structInterfaceValue(s, "main.Simple", "main.Simple")) as [$.Slice<number>, $.GoError])
Expand All @@ -40,6 +102,35 @@ export async function main(): globalThis.Promise<void> {
} else {
await $.println("Result:", $.bytesToString(b))
}
await unmarshalNames()
}

export async function unmarshalNames(): globalThis.Promise<void> {
let r: $.VarRef<Ref> = $.varRef(new Ref(true))
let err: $.GoError = (json.Unmarshal(new Uint8Array([123, 34, 111, 98, 106, 101, 99, 116, 34, 58, 123, 34, 115, 104, 97, 34, 58, 34, 97, 98, 99, 34, 44, 34, 84, 89, 80, 69, 34, 58, 34, 99, 111, 109, 109, 105, 116, 34, 125, 44, 34, 76, 65, 66, 69, 76, 34, 58, 34, 109, 97, 105, 110, 34, 125]), $.interfaceValue(r, "*main.Ref", /* @__PURE__ */ $.pointerType("main.Ref"))) as $.GoError)
await $.println("named:", err == null, r.value.Object.SHA, r.value.Object.Type, r.value.Label)

let a: $.VarRef<{"Object": {"SHA": string}}> = $.varRef($.anonymousStructValue({"Object": $.anonymousStructValue({"SHA": ""}, { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("SHA", /* @__PURE__ */ $.basicType("string"), [0], 0, true)] })}, { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("Object", { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("SHA", /* @__PURE__ */ $.basicType("string"), [0], 0, true)] }, [0], 0, true)] }))
err = (json.Unmarshal(new Uint8Array([123, 34, 111, 98, 106, 101, 99, 116, 34, 58, 123, 34, 115, 104, 97, 34, 58, 34, 100, 101, 102, 34, 125, 125]), $.interfaceValue(a, "*struct{Object struct{SHA string}}", /* @__PURE__ */ $.pointerType({ kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("Object", { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("SHA", /* @__PURE__ */ $.basicType("string"), [0], 0, true)] }, [0], 0, true)] }))) as $.GoError)
await $.println("anonymous:", err == null, a.value.Object.SHA)

let o: $.VarRef<Object> = $.varRef(new Object(true))
err = (json.Unmarshal(new Uint8Array([123, 34, 115, 104, 97, 34, 58, 34, 120, 34, 44, 34, 83, 72, 65, 34, 58, 34, 121, 34, 44, 34, 116, 121, 112, 101, 34, 58, 34, 116, 34, 125]), $.interfaceValue(o, "*main.Object", /* @__PURE__ */ $.pointerType("main.Object"))) as $.GoError)
await $.println("both cases:", err == null, o.value.SHA, o.value.Type)

let d: json.Decoder | $.VarRef<json.Decoder> | null = json.NewDecoder($.pointerValueOrNil($.interfaceValue<io.Reader | null>(strings.NewReader("{\"sha\":\"z\",\"other\":1}"), "*strings.Reader", __goscriptStatic0()))!)
void (d !== null && (d as any).__isVarRef !== true && (d as any).__goValue === undefined ? ((d as any) as json.Decoder).DisallowUnknownFields() : json.Decoder.prototype.DisallowUnknownFields.call($.pointerValue<json.Decoder>(d)))
err = await (d !== null && (d as any).__isVarRef !== true && (d as any).__goValue === undefined ? ((d as any) as json.Decoder).Decode($.interfaceValue(o, "*main.Object", /* @__PURE__ */ $.pointerType("main.Object"))) : json.Decoder.prototype.Decode.call($.pointerValue<json.Decoder>(d), $.interfaceValue(o, "*main.Object", /* @__PURE__ */ $.pointerType("main.Object"))))
await $.println("unknown:", err != null)
d = json.NewDecoder($.pointerValueOrNil($.interfaceValue<io.Reader | null>(strings.NewReader("{\"sha\":\"w\"}"), "*strings.Reader", __goscriptStatic0()))!)
void (d !== null && (d as any).__isVarRef !== true && (d as any).__goValue === undefined ? ((d as any) as json.Decoder).DisallowUnknownFields() : json.Decoder.prototype.DisallowUnknownFields.call($.pointerValue<json.Decoder>(d)))
err = await (d !== null && (d as any).__isVarRef !== true && (d as any).__goValue === undefined ? ((d as any) as json.Decoder).Decode($.interfaceValue(o, "*main.Object", /* @__PURE__ */ $.pointerType("main.Object"))) : json.Decoder.prototype.Decode.call($.pointerValue<json.Decoder>(d), $.interfaceValue(o, "*main.Object", /* @__PURE__ */ $.pointerType("main.Object"))))
await $.println("known:", err == null, o.value.SHA)
}

var __goscriptStatic0$value: any
function __goscriptStatic0(): any {
return __goscriptStatic0$value ??= ($.overrideTypeMethods(/* @__PURE__ */ $.pointerType("strings.Reader"), "strings.Reader", () => [$.methodSignature("Len", [], [/* @__PURE__ */ $.basicType("int")], true), $.methodSignature("Read", [["b", /* @__PURE__ */ $.sliceType(/* @__PURE__ */ $.basicType("uint8"))]], [["n", /* @__PURE__ */ $.basicType("int")], ["err", "error"]], true), $.methodSignature("ReadAt", [["b", /* @__PURE__ */ $.sliceType(/* @__PURE__ */ $.basicType("uint8"))], ["off", /* @__PURE__ */ $.basicType("int64")]], [["n", /* @__PURE__ */ $.basicType("int")], ["err", "error"]], true), $.methodSignature("ReadByte", [], [/* @__PURE__ */ $.basicType("uint8"), "error"], true), $.methodSignature("ReadRune", [], [["ch", /* @__PURE__ */ $.basicType("int32")], ["size", /* @__PURE__ */ $.basicType("int")], ["err", "error"]], true), $.methodSignature("Reset", [["s", /* @__PURE__ */ $.basicType("string")]], [], true), $.methodSignature("Seek", [["offset", /* @__PURE__ */ $.basicType("int64")], ["whence", /* @__PURE__ */ $.basicType("int")]], [/* @__PURE__ */ $.basicType("int64"), "error"], true), $.methodSignature("Size", [], [/* @__PURE__ */ $.basicType("int64")], true), $.methodSignature("UnreadByte", [], ["error"], true), $.methodSignature("UnreadRune", [], ["error"], true), $.methodSignature("WriteTo", [["w", "io.Writer"]], [["n", /* @__PURE__ */ $.basicType("int64")], ["err", "error"]], true)]))
}

var __goscriptAwait: any
Expand Down
Loading