{"record":{"id":"4aca7f9b5083e19c","repo":"pinpoint-apm/pinpoint","slug":"n-n-relationship-between-fields-and-tags-is-not-al","errorCode":null,"errorMessage":"N:N relationship between fields and tags is not allowed.","messagePattern":"N:N relationship between fields and tags is not allowed\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"otlpmetric/otlpmetric-common/src/main/java/com/navercorp/pinpoint/otlp/common/web/defined/AppMetricDefinitionUtil.java","lineNumber":42,"sourceCode":"import java.util.stream.Collectors;\n\n/**\n * @author minwoo-jung\n */\npublic class AppMetricDefinitionUtil {\n\n    static public void validate(List<AppMetricDefinition> appMetricDefinitionList) {\n        for (AppMetricDefinition appMetricDefinition : appMetricDefinitionList) {\n            List<String> tagGroupList = appMetricDefinition.getTagGroupList();\n            List<String> fieldNameList = appMetricDefinition.getFieldNameList();\n\n            validateCountOfTagAndField(tagGroupList, fieldNameList);\n        }\n    }\n\n    static public void validateCountOfTagAndField(List<String> tagGroupList, List<String> fieldNameList) {\n        if (tagGroupList.size() > 1 && fieldNameList.size() > 1) {\n            throw new IllegalArgumentException(\"N:N relationship between fields and tags is not allowed.\");\n        }\n\n        if (tagGroupList.size() != 1 && fieldNameList.size() != 1) {\n            throw new IllegalArgumentException(\"Either tagGroupList or fieldNameList must have a size of exactly one.\");\n        }\n    }\n\n    static public void generateAndSetUniqueId(List<AppMetricDefinition> appMetricDefinitionList) {\n        Set<String> existingIds = appMetricDefinitionList.stream()\n                .map(AppMetricDefinition::getId)\n                .filter(StringUtils::hasLength)\n                .collect(Collectors.toSet());\n\n        appMetricDefinitionList.stream().filter(appMetricDefinition -> StringUtils.isEmpty(appMetricDefinition.getId()))\n                .forEach(definition -> {\n                    String newId;\n\n                    do {","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/pinpoint-apm/pinpoint/blob/744c3d3075e595656abb1ae331ad2c0e4c9eb996/otlpmetric/otlpmetric-common/src/main/java/com/navercorp/pinpoint/otlp/common/web/defined/AppMetricDefinitionUtil.java#L24-L60","documentation":"AppMetricDefinitionUtil.validateCountOfTagAndField enforces the cardinality rules for Pinpoint OTLP application-defined metrics: the relation between tag groups and fields must be 1:N (one tag group, many fields) or N:1 (many tag groups, one field). If both tagGroupList and fieldNameList have more than one element, the N:N case is rejected with IllegalArgumentException because the query/legend model cannot represent it.","triggerScenarios":"Saving or validating an AppMetricDefinition (via the web metric-definition API) where the definition declares multiple tagGroups AND multiple fieldNames simultaneously — e.g. a metric definition JSON with tagGroup [\"a\",\"b\"] and fieldName [\"f1\",\"f2\"].","commonSituations":"1) User builds a metric definition in the Pinpoint web UI selecting several tag groupings and several fields at once. 2) Programmatically POSTing /api/otlp metric definitions with hand-written JSON where both lists have >1 entries. 3) Importing definitions exported from a different schema version.","solutions":["Split the definition: create one AppMetricDefinition per (tagGroup, field) pair, keeping cardinality 1:N or N:1.","Reduce the definition to multiple tag groups with a single field, or a single tag group with multiple fields.","Validate the definition payload client-side before calling the save/validate API.","If the N:N case is genuinely needed, model it as separate definitions or request a schema change upstream."],"exampleFix":"// before\nnew AppMetricDefinition(tagGroups=[\"host\",\"region\"], fieldNames=[\"cpu\",\"mem\"], ...)\n// after — one definition per pair\nnew AppMetricDefinition(tagGroups=[\"host\"], fieldNames=[\"cpu\",\"mem\"], ...)\nnew AppMetricDefinition(tagGroups=[\"host\",\"region\"], fieldNames=[\"cpu\"], ...)","handlingStrategy":"validation","validationCode":"function isValidDefinition(def) {\n  const tg = def.tagGroups?.length ?? 0;\n  const fn = def.fieldNames?.length ?? 0;\n  return !(tg > 1 && fn > 1) && (tg === 1 || fn === 1);\n}\nif (!isValidDefinition(payload)) throw new Error(\"Use 1:N or N:1 tag/field cardinality\");","typeGuard":"boolean isOneToOneSide(List<String> tagGroups, List<String> fieldNames) {\n    return !(tagGroups.size() > 1 && fieldNames.size() > 1)\n        && (tagGroups.size() == 1 || fieldNames.size() == 1);\n}","tryCatchPattern":"try {\n    appMetricDefinitionService.save(def);\n} catch (IllegalArgumentException e) {\n    return badRequest(\"Metric definition cardinality invalid: \" + e.getMessage());\n}","preventionTips":["Design definitions as one tag group with many fields, or many tag groups with one field.","Validate definitions in the UI before allowing submit.","Keep a JSON schema for AppMetricDefinition payloads and validate on the client.","When migrating from other APM metric models, decompose N:N mappings into multiple definitions."],"tags":["java","validation","pinpoint","metric-definition","cardinality"],"backgroundTag":"invalid-argument-value","analyzedSha":"744c3d3075e595656abb1ae331ad2c0e4c9eb996","analyzedAt":"2026-09-07T18:48:45.289Z","contentChangedAt":"2026-09-07T18:48:45.289Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}