{"record":{"id":"11a66c3946a6b2a6","repo":"expo/expo","slug":"font-family-json-stringify-fontfamily-declares-11a66c","errorCode":null,"errorMessage":"Font family ${JSON.stringify(fontFamily)} declares two fonts for weight ${definition.weight} and style ${JSON.stringify(style)}: ${alreadyDeclaredBy} and ${definition.path}. Android matches a family on weight and style alone, so the app would crash on startup while registering it. Give each definition a weight or style of its own — \"axes\" cannot tell them apart.","messagePattern":"Font family (.+?) declares two fonts for weight (.+?) and style (.+?): (.+?) and (.+?)\\. Android matches a family on weight and style alone, so the app would crash on startup while registering it\\. Give each definition a weight or style of its own — \"axes\" cannot tell them apart\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/expo-font/plugin/src/withFontsAndroid.ts","lineNumber":129,"sourceCode":"}\n\n/**\n * Throws when two definitions in the same family claim the same weight and style.\n *\n * Android resolves a family by (weight, style), and `FontFamily.Builder.addFont` rejects a second\n * font carrying a pair the family already holds.\n */\nexport function assertNoConflictingDefinitions(fontsByFamily: GroupedFontObject) {\n  for (const [fontFamily, definitions] of Object.entries(fontsByFamily)) {\n    const pathByWeightAndStyle = new Map<string, string>();\n\n    for (const definition of definitions) {\n      const style = definition.style || 'normal';\n      const key = `${definition.weight}/${style}`;\n      const alreadyDeclaredBy = pathByWeightAndStyle.get(key);\n\n      if (alreadyDeclaredBy) {\n        throw new Error(\n          `Font family ${JSON.stringify(fontFamily)} declares two fonts for weight ${definition.weight} and style ${JSON.stringify(style)}: ${alreadyDeclaredBy} and ${definition.path}. ` +\n            `Android matches a family on weight and style alone, so the app would crash on startup while registering it. ` +\n            `Give each definition a weight or style of its own — \"axes\" cannot tell them apart.`\n        );\n      }\n\n      pathByWeightAndStyle.set(key, definition.path);\n    }\n  }\n}\n\n// https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg\nconst AXIS_TAG_LENGTH = 4;\nconst AXIS_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9]* *$/;\n\n// Case splits the namespace: registered axes are lowercase, a font's own axes are uppercase, and a\n// tag in any other case names nothing. So `SLNT` is a font's own axis, not a misspelt `slnt`.\nconst registeredAxisTags = ['ital', 'opsz', 'slnt', 'wdth', 'wght'];","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/expo/expo/blob/da586c407bd53c4369a994e6ed5d6cf3340420f5/packages/expo-font/plugin/src/withFontsAndroid.ts#L111-L147","documentation":"Thrown by the expo-font Android config plugin (assertNoConflictingDefinitions, packages/expo-font/plugin/src/withFontsAndroid.ts:129) when two font definitions inside the same fontFamily claim the same weight and style pair. Android's FontFamily.Builder.addFont keys each family member by (weight, style) alone, so a duplicate pair would make the app crash on startup while registering the family — the plugin fails the prebuild instead. Note that 'axes' (fontVariationSettings) cannot disambiguate the two entries.","triggerScenarios":"Passing a FontObject to useFonts (or withFonts in app.json/plugins) whose fontDefinitions contain two entries with the same weight and the same style (style defaults to 'normal'): e.g. a variable font file reused for two definitions where only 'axes' differs, or a copy-pasted definition where 'weight' was not updated. assertNoConflictingDefinitions runs during addXmlFonts at config-plugin time, so this fires on prebuild/compile, not at runtime.","commonSituations":"Using one variable font file (e.g. Inter[wght].ttf) to back several weights and forgetting to change weight on the copy; migrating a config from a system where variation settings distinguished faces; adding a 'display' variant with the same 400 weight as the regular face.","solutions":["Give each fontDefinitions entry in that family a unique weight (e.g. 400 and 700)","Or set style: 'italic' on one of the two conflicting definitions","If you genuinely need two faces at the same weight and style, split them into two different fontFamily names","Re-run prebuild (npx expo prebuild --clean) after fixing to confirm the plugin passes"],"exampleFix":"// before (app.json expo-font plugin input)\n{\n  \"fontFamily\": \"Inter\",\n  \"fontDefinitions\": [\n    { \"path\": \"./Inter-Var.ttf\", \"weight\": 400, \"axes\": { \"opsz\": 14 } },\n    { \"path\": \"./Inter-Var.ttf\", \"weight\": 400, \"axes\": { \"opsz\": 32 } }\n  ]\n}\n// after — unique weight per definition\n{\n  \"fontFamily\": \"Inter\",\n  \"fontDefinitions\": [\n    { \"path\": \"./Inter-Var.ttf\", \"weight\": 400, \"axes\": { \"opsz\": 14 } },\n    { \"path\": \"./Inter-Var.ttf\", \"weight\": 500, \"axes\": { \"opsz\": 32 } }\n  ]\n}","handlingStrategy":"validation","validationCode":"import type { FontObject } from 'expo-font';\n\nfunction assertNoDuplicateWeightStyle(fonts: FontObject[]) {\n  for (const { fontFamily, fontDefinitions } of fonts) {\n    const seen = new Map<string, string>();\n    for (const d of fontDefinitions) {\n      const key = `${d.weight}/${d.style || 'normal'}`;\n      const prev = seen.get(key);\n      if (prev) {\n        throw new Error(`${fontFamily}: ${prev} and ${d.path} both claim weight ${d.weight} / style ${d.style || 'normal'}`);\n      }\n      seen.set(key, d.path);\n    }\n  }\n}\n// run before passing fonts to the plugin / app.json\nassertNoDuplicateWeightStyle(fonts);","typeGuard":"function hasUniqueWeightStylePairs(defs: { weight?: number; style?: string; path?: string }[]): boolean {\n  const keys = defs.map((d) => `${d.weight}/${d.style || 'normal'}`);\n  return new Set(keys).size === keys.length;\n}","tryCatchPattern":null,"preventionTips":["Keep one source of truth for the font list and lint it in CI with the duplicate check above","When reusing a variable font file for several weights, template the definitions from an array of weights so the weight always varies","Remember style defaults to 'normal': two definitions differing only in axes still conflict"],"tags":["expo-font","android","config-plugin","fonts","variable-fonts"],"backgroundTag":"duplicate-font-definitions","analyzedSha":"da586c407bd53c4369a994e6ed5d6cf3340420f5","analyzedAt":"2026-08-23T00:40:06.936Z","contentChangedAt":"2026-08-23T00:40:06.936Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}