diff --git a/crates/libsyntax2/src/grammar/items/mod.rs b/crates/libsyntax2/src/grammar/items/mod.rs
index 8c19aa17955..2567313ab1c 100644
--- a/crates/libsyntax2/src/grammar/items/mod.rs
+++ b/crates/libsyntax2/src/grammar/items/mod.rs
@@ -181,7 +181,16 @@ fn items_without_modifiers(p: &mut Parser) -> Option<SyntaxKind> {
             MODULE
         }
         STRUCT_KW => {
-            nominal::struct_def(p);
+            // test struct_items
+            // struct Foo;
+            // struct Foo {}
+            // struct Foo();
+            // struct Foo(String, usize);
+            // struct Foo {
+            //     a: i32,
+            //     b: f32,
+            // }
+            nominal::struct_def(p, STRUCT_KW);
             if p.at(SEMI) {
                 p.err_and_bump(
                     "expected item, found `;`\n\
@@ -190,6 +199,16 @@ fn items_without_modifiers(p: &mut Parser) -> Option<SyntaxKind> {
             }
             STRUCT_DEF
         }
+        IDENT if p.at_contextual_kw("union") => {
+            // test union_items
+            // union Foo {}
+            // union Foo {
+            //     a: i32,
+            //     b: f32,
+            // }
+            nominal::struct_def(p, UNION_KW);
+            STRUCT_DEF
+        }
         ENUM_KW => {
             nominal::enum_def(p);
             ENUM_DEF
diff --git a/crates/libsyntax2/src/grammar/items/nominal.rs b/crates/libsyntax2/src/grammar/items/nominal.rs
index 11c43e371e9..8d02ad555a3 100644
--- a/crates/libsyntax2/src/grammar/items/nominal.rs
+++ b/crates/libsyntax2/src/grammar/items/nominal.rs
@@ -1,8 +1,8 @@
 use super::*;
 
-pub(super) fn struct_def(p: &mut Parser) {
-    assert!(p.at(STRUCT_KW));
-    p.bump();
+pub(super) fn struct_def(p: &mut Parser, kind: SyntaxKind) {
+    assert!(p.at(STRUCT_KW) || p.at_contextual_kw("union"));
+    p.bump_remap(kind);
 
     name_r(p, ITEM_RECOVERY_SET);
     type_params::opt_type_param_list(p);
@@ -22,19 +22,23 @@ pub(super) fn struct_def(p: &mut Parser) {
                 }
             }
         }
-        SEMI => {
+        SEMI if kind == STRUCT_KW => {
             p.bump();
             return;
         }
         L_CURLY => named_field_def_list(p),
-        L_PAREN => {
+        L_PAREN if kind == STRUCT_KW => {
             pos_field_list(p);
             p.expect(SEMI);
         }
-        _ => {
+        _ if kind == STRUCT_KW => {
             p.error("expected `;`, `{`, or `(`");
             return;
         }
+        _ => {
+            p.error("expected `{`");
+            return;
+        }
     }
 }
 
diff --git a/crates/libsyntax2/tests/data/parser/inline/0109_struct_items.txt b/crates/libsyntax2/tests/data/parser/inline/0109_struct_items.txt
new file mode 100644
index 00000000000..ea016d393df
--- /dev/null
+++ b/crates/libsyntax2/tests/data/parser/inline/0109_struct_items.txt
@@ -0,0 +1,86 @@
+ROOT@[0; 105)
+  STRUCT_DEF@[0; 11)
+    STRUCT_KW@[0; 6)
+    WHITESPACE@[6; 7)
+    NAME@[7; 10)
+      IDENT@[7; 10) "Foo"
+    SEMI@[10; 11)
+  WHITESPACE@[11; 12)
+  STRUCT_DEF@[12; 25)
+    STRUCT_KW@[12; 18)
+    WHITESPACE@[18; 19)
+    NAME@[19; 22)
+      IDENT@[19; 22) "Foo"
+    WHITESPACE@[22; 23)
+    NAMED_FIELD_DEF_LIST@[23; 25)
+      L_CURLY@[23; 24)
+      R_CURLY@[24; 25)
+  WHITESPACE@[25; 26)
+  STRUCT_DEF@[26; 39)
+    STRUCT_KW@[26; 32)
+    WHITESPACE@[32; 33)
+    NAME@[33; 36)
+      IDENT@[33; 36) "Foo"
+    POS_FIELD_LIST@[36; 38)
+      L_PAREN@[36; 37)
+      R_PAREN@[37; 38)
+    SEMI@[38; 39)
+  WHITESPACE@[39; 40)
+  STRUCT_DEF@[40; 66)
+    STRUCT_KW@[40; 46)
+    WHITESPACE@[46; 47)
+    NAME@[47; 50)
+      IDENT@[47; 50) "Foo"
+    POS_FIELD_LIST@[50; 65)
+      L_PAREN@[50; 51)
+      POS_FIELD@[51; 57)
+        PATH_TYPE@[51; 57)
+          PATH@[51; 57)
+            PATH_SEGMENT@[51; 57)
+              NAME_REF@[51; 57)
+                IDENT@[51; 57) "String"
+      COMMA@[57; 58)
+      WHITESPACE@[58; 59)
+      POS_FIELD@[59; 64)
+        PATH_TYPE@[59; 64)
+          PATH@[59; 64)
+            PATH_SEGMENT@[59; 64)
+              NAME_REF@[59; 64)
+                IDENT@[59; 64) "usize"
+      R_PAREN@[64; 65)
+    SEMI@[65; 66)
+  WHITESPACE@[66; 67)
+  STRUCT_DEF@[67; 105)
+    STRUCT_KW@[67; 73)
+    WHITESPACE@[73; 74)
+    NAME@[74; 77)
+      IDENT@[74; 77) "Foo"
+    WHITESPACE@[77; 78)
+    NAMED_FIELD_DEF_LIST@[78; 105)
+      L_CURLY@[78; 79)
+      WHITESPACE@[79; 84)
+      NAMED_FIELD_DEF@[84; 90)
+        NAME@[84; 85)
+          IDENT@[84; 85) "a"
+        COLON@[85; 86)
+        WHITESPACE@[86; 87)
+        PATH_TYPE@[87; 90)
+          PATH@[87; 90)
+            PATH_SEGMENT@[87; 90)
+              NAME_REF@[87; 90)
+                IDENT@[87; 90) "i32"
+      COMMA@[90; 91)
+      WHITESPACE@[91; 96)
+      NAMED_FIELD_DEF@[96; 102)
+        NAME@[96; 97)
+          IDENT@[96; 97) "b"
+        COLON@[97; 98)
+        WHITESPACE@[98; 99)
+        PATH_TYPE@[99; 102)
+          PATH@[99; 102)
+            PATH_SEGMENT@[99; 102)
+              NAME_REF@[99; 102)
+                IDENT@[99; 102) "f32"
+      COMMA@[102; 103)
+      WHITESPACE@[103; 104)
+      R_CURLY@[104; 105)
diff --git a/crates/libsyntax2/tests/data/parser/inline/0110_union_items.txt b/crates/libsyntax2/tests/data/parser/inline/0110_union_items.txt
new file mode 100644
index 00000000000..93e8a4e8de7
--- /dev/null
+++ b/crates/libsyntax2/tests/data/parser/inline/0110_union_items.txt
@@ -0,0 +1,45 @@
+ROOT@[0; 50)
+  STRUCT_DEF@[0; 12)
+    UNION_KW@[0; 5)
+    WHITESPACE@[5; 6)
+    NAME@[6; 9)
+      IDENT@[6; 9) "Foo"
+    WHITESPACE@[9; 10)
+    NAMED_FIELD_DEF_LIST@[10; 12)
+      L_CURLY@[10; 11)
+      R_CURLY@[11; 12)
+  WHITESPACE@[12; 13)
+  STRUCT_DEF@[13; 50)
+    UNION_KW@[13; 18)
+    WHITESPACE@[18; 19)
+    NAME@[19; 22)
+      IDENT@[19; 22) "Foo"
+    WHITESPACE@[22; 23)
+    NAMED_FIELD_DEF_LIST@[23; 50)
+      L_CURLY@[23; 24)
+      WHITESPACE@[24; 29)
+      NAMED_FIELD_DEF@[29; 35)
+        NAME@[29; 30)
+          IDENT@[29; 30) "a"
+        COLON@[30; 31)
+        WHITESPACE@[31; 32)
+        PATH_TYPE@[32; 35)
+          PATH@[32; 35)
+            PATH_SEGMENT@[32; 35)
+              NAME_REF@[32; 35)
+                IDENT@[32; 35) "i32"
+      COMMA@[35; 36)
+      WHITESPACE@[36; 41)
+      NAMED_FIELD_DEF@[41; 47)
+        NAME@[41; 42)
+          IDENT@[41; 42) "b"
+        COLON@[42; 43)
+        WHITESPACE@[43; 44)
+        PATH_TYPE@[44; 47)
+          PATH@[44; 47)
+            PATH_SEGMENT@[44; 47)
+              NAME_REF@[44; 47)
+                IDENT@[44; 47) "f32"
+      COMMA@[47; 48)
+      WHITESPACE@[48; 49)
+      R_CURLY@[49; 50)