{"record":{"id":"b4a435aff30cdea2","repo":"Tencent/APIJSON","slug":"join-e-getkey-k-app-join-ke","errorCode":null,"errorMessage":"join:'${e.getKey()}' 中 ${k} 不合法 ！@ APP JOIN 只允许 key@:/Table/refKey 这种 = 等价连接！","messagePattern":"join:'(.+?)' 中 (.+?) 不合法 ！@ APP JOIN 只允许 key@:/Table/refKey 这种 = 等价连接！","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"APIJSONORM/src/main/java/apijson/orm/AbstractParser.java","lineNumber":1740,"sourceCode":"\t\t\t\tif (k.length() > 1 && k.indexOf(\"@\") == k.length() - 1 && v instanceof String) {\n\t\t\t\t\tString sv = (String) v;\n\t\t\t\t\tint ind = sv.endsWith(\"@\") ? -1 : sv.indexOf(\"/\");\n\t\t\t\t\tif (ind == 0 && key == null) {  // 指定了某个就只允许一个 ON 条件\n\t\t\t\t\t\tString p = sv.substring(1);\n\t\t\t\t\t\tint ind2 = p.indexOf(\"/\");\n\t\t\t\t\t\tString tk = ind2 < 0 ? null : p.substring(0, ind2);\n\n\t\t\t\t\t\tapijson.orm.Entry<String, String> te = tk == null || p.substring(ind2 + 1).indexOf(\"/\") >= 0 ? null : Pair.parseEntry(tk, true);\n\n\t\t\t\t\t\tif (te != null && isTableKey(te.getKey()) && request.get(tk) instanceof Map<?, ?>) {\n\t\t\t\t\t\t\tif (isAppJoin) {\n\t\t\t\t\t\t\t\tif (refObj.size() >= 1) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":\" + e.getKey() + \" 中 \" + k + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t\t+ \"@ APP JOIN 必须有且只有一个引用赋值键值对！\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (StringUtil.isName(k.substring(0, k.length() - 1)) == false) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":'\" + e.getKey() + \"' 中 \" + k + \" 不合法 ！\" +\n\t\t\t\t\t\t\t\t\t\t\t\"@ APP JOIN 只允许 key@:/Table/refKey 这种 = 等价连接！\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\trefObj.put(k, v);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tObject rv = getValueByPath(sv);\n\t\t\t\t\tif (rv != null && rv.equals(sv) == false) {\n\t\t\t\t\t\trequestObj.put(k.substring(0, k.length() - 1), rv);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tthrow new UnsupportedOperationException(table + \"/\" + k + \" 不合法！\" + apijson.JSONRequest.KEY_JOIN + \" 关联的 Table 中，\"\n\t\t\t\t\t\t\t+ \"join: ?/Table/key 时只能有 1 个 key@:value；join: ?/Table 时所有 key@:value 要么是符合 join 格式，要么能直接解析成具体值！\");  // TODO 支持 join on\n\t\t\t\t}","sourceCodeStart":1722,"sourceCodeEnd":1758,"githubUrl":"https://github.com/Tencent/APIJSON/blob/5284052872898eddc449a58f629e5c8d588b8e22/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java#L1722-L1758","documentation":"Same identifier rule as the explicit-key variant, but applied to keys discovered while scanning the joined table object for APP JOIN: the candidate key 'k' minus its trailing '@' must pass StringUtil.isName. If it does not, this IllegalArgumentException is thrown because '@' joins only permit key@:/Table/refKey '=' connections.","triggerScenarios":"{ 'join': '@/Comment', 'Comment': { 'user-id@': '/User/id' } } — the reference key 'user-id@' has a hyphen in its name part; likewise keys starting with a digit, containing spaces, '@', or other punctuation.","commonSituations":"Auto-generating request keys from DB column names that contain hyphens; mixing camelCase and kebab-case during refactor; a template engine emitting malformed key names.","solutions":["Rename the reference key to a valid identifier (letter first, then letters/digits/underscore): 'user-id@' -> 'userId@'","Keep the key and value consistent: the same key must be the single ON condition of the '@' join"],"exampleFix":"// before\n{ 'join': '@/Comment', 'Comment': { 'user-id@': '/User/id' } }\n// after\n{ 'join': '@/Comment', 'Comment': { 'userId@': '/User/id' } }","handlingStrategy":"validation","validationCode":"const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\nfunction checkRefKeys(joinValue) {\n  const paths = typeof joinValue === 'string' ? [joinValue] : Object.keys(joinValue);\n  for (const p of paths.filter(x => x.startsWith('@'))) {\n    const tblKey = p.split('/')[1];\n    // caller checks req[tblKey] keys:\n  }\n  return null;\n}\n// apply to table object: Object.keys(tbl).filter(k => k.endsWith('@')).every(k => NAME_RE.test(k.slice(0, -1)))","typeGuard":"const isValidRefKey = (k) => /^[A-Za-z_][A-Za-z0-9_]*@$/.test(k);","tryCatchPattern":"Catch IllegalArgumentException with 'APP JOIN 只允许 key@:/Table/refKey'; surface which key failed (named in message).","preventionTips":["Lint every key@ in joined tables against an identifier regex before sending","Avoid generating key names from raw DB column names with hyphens/spaces"],"tags":["apijson","join","app-join","identifier","validation"],"backgroundTag":null,"analyzedSha":"5284052872898eddc449a58f629e5c8d588b8e22","analyzedAt":"2026-08-14T15:15:29.577Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}